completes kotlin migration

Fri, 02 Apr 2021 11:59:14 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 02 Apr 2021 11:59:14 +0200
changeset 184
e8eecee6aadf
parent 183
61669abf277f
child 185
5ec9fcfbdf9c

completes kotlin migration

src/main/java/de/uapcore/lightpit/AbstractServlet.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/java/de/uapcore/lightpit/viewmodel/ComponentEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ComponentInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ComponentsView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueDetailView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetailsView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/Constants.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DaoFactory.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Component.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/IssueSummary.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Project.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Version.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/filter/Filter.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/filter/IssueFilter.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/ErrorServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/LanguageServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/util/Filter.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/util/Issues.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Components.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/LanguageView.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Users.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/component-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/components.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/error.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-view.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-navmenu.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/projects.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/user-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/users.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/versions.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/navmenu.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/project-header.jspf file | annotate | diff | comparison | revisions
     1.1 --- a/src/main/java/de/uapcore/lightpit/AbstractServlet.java	Sat Jan 23 14:47:59 2021 +0100
     1.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.3 @@ -1,460 +0,0 @@
     1.4 -/*
     1.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     1.6 - *
     1.7 - * Copyright 2021 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 AbstractServlet extends HttpServlet {
    1.57 -
    1.58 -    private static final Logger LOG = LoggerFactory.getLogger(AbstractServlet.class);
    1.59 -
    1.60 -    /**
    1.61 -     * Invocation mapping gathered from the {@link RequestMapping} annotations.
    1.62 -     * <p>
    1.63 -     * Paths in this map must always start with a leading slash, although
    1.64 -     * the specification in the annotation must not start with a leading slash.
    1.65 -     * <p>
    1.66 -     * The reason for this is the different handling of empty paths in
    1.67 -     * {@link HttpServletRequest#getPathInfo()}.
    1.68 -     */
    1.69 -    private final Map<HttpMethod, Map<PathPattern, Method>> mappings = new HashMap<>();
    1.70 -
    1.71 -    /**
    1.72 -     * Creates a set of data access objects for the specified connection.
    1.73 -     *
    1.74 -     * @param connection the SQL connection
    1.75 -     * @return a set of data access objects
    1.76 -     */
    1.77 -    private DataAccessObject createDataAccessObjects(Connection connection) {
    1.78 -        final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
    1.79 -        if (df.getDialect() == DataSourceProvider.Dialect.Postgres) {
    1.80 -            return new PostgresDataAccessObject(connection);
    1.81 -        }
    1.82 -        throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug.");
    1.83 -    }
    1.84 -
    1.85 -    private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException {
    1.86 -        final var pathPattern = mapping.getKey();
    1.87 -        final var method = mapping.getValue();
    1.88 -        try {
    1.89 -            LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
    1.90 -            final var paramTypes = method.getParameterTypes();
    1.91 -            final var paramValues = new Object[paramTypes.length];
    1.92 -            for (int i = 0; i < paramTypes.length; i++) {
    1.93 -                if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
    1.94 -                    paramValues[i] = req;
    1.95 -                } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
    1.96 -                    paramValues[i] = resp;
    1.97 -                }
    1.98 -                if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) {
    1.99 -                    paramValues[i] = dao;
   1.100 -                }
   1.101 -                if (paramTypes[i].isAssignableFrom(PathParameters.class)) {
   1.102 -                    paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req));
   1.103 -                }
   1.104 -            }
   1.105 -            method.invoke(this, paramValues);
   1.106 -        } catch (InvocationTargetException ex) {
   1.107 -            LOG.error("invocation of method {}::{} failed: {}",
   1.108 -                    method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage());
   1.109 -            LOG.debug("Details: ", ex.getTargetException());
   1.110 -            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage());
   1.111 -        } catch (ReflectiveOperationException | ClassCastException ex) {
   1.112 -            LOG.error("invocation of method {}::{} failed: {}",
   1.113 -                    method.getDeclaringClass().getName(), method.getName(), ex.getMessage());
   1.114 -            LOG.debug("Details: ", ex);
   1.115 -            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
   1.116 -        }
   1.117 -    }
   1.118 -
   1.119 -    @Override
   1.120 -    public void init() throws ServletException {
   1.121 -        scanForRequestMappings();
   1.122 -
   1.123 -        LOG.trace("{} initialized", getServletName());
   1.124 -    }
   1.125 -
   1.126 -    private void scanForRequestMappings() {
   1.127 -        try {
   1.128 -            Method[] methods = getClass().getDeclaredMethods();
   1.129 -            for (Method method : methods) {
   1.130 -                Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
   1.131 -                if (mapping.isPresent()) {
   1.132 -                    if (mapping.get().requestPath().isBlank()) {
   1.133 -                        LOG.warn("{} is annotated with {} but request path is empty",
   1.134 -                                method.getName(), RequestMapping.class.getSimpleName()
   1.135 -                        );
   1.136 -                        continue;
   1.137 -                    }
   1.138 -
   1.139 -                    if (!Modifier.isPublic(method.getModifiers())) {
   1.140 -                        LOG.warn("{} is annotated with {} but is not public",
   1.141 -                                method.getName(), RequestMapping.class.getSimpleName()
   1.142 -                        );
   1.143 -                        continue;
   1.144 -                    }
   1.145 -                    if (Modifier.isAbstract(method.getModifiers())) {
   1.146 -                        LOG.warn("{} is annotated with {} but is abstract",
   1.147 -                                method.getName(), RequestMapping.class.getSimpleName()
   1.148 -                        );
   1.149 -                        continue;
   1.150 -                    }
   1.151 -
   1.152 -                    boolean paramsInjectible = true;
   1.153 -                    for (var param : method.getParameterTypes()) {
   1.154 -                        paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
   1.155 -                                            || HttpServletResponse.class.isAssignableFrom(param)
   1.156 -                                            || PathParameters.class.isAssignableFrom(param)
   1.157 -                                            || DataAccessObject.class.isAssignableFrom(param);
   1.158 -                    }
   1.159 -                    if (paramsInjectible) {
   1.160 -                        try {
   1.161 -                            PathPattern pathPattern = new PathPattern(mapping.get().requestPath());
   1.162 -
   1.163 -                            final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>());
   1.164 -                            final var currentMapping = methodMappings.putIfAbsent(pathPattern, method);
   1.165 -                            if (currentMapping != null) {
   1.166 -                                LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}",
   1.167 -                                        mapping.get().method(),
   1.168 -                                        mapping.get().requestPath(),
   1.169 -                                        method.getName(),
   1.170 -                                        getClass().getSimpleName(),
   1.171 -                                        currentMapping.getName()
   1.172 -                                );
   1.173 -                            }
   1.174 -
   1.175 -                            LOG.debug("{} {} maps to {}::{}",
   1.176 -                                    mapping.get().method(),
   1.177 -                                    mapping.get().requestPath(),
   1.178 -                                    getClass().getSimpleName(),
   1.179 -                                    method.getName()
   1.180 -                            );
   1.181 -                        } catch (IllegalArgumentException ex) {
   1.182 -                            LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid",
   1.183 -                                    method.getName(), mapping.get().requestPath()
   1.184 -                            );
   1.185 -                        }
   1.186 -                    } else {
   1.187 -                        LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed",
   1.188 -                                method.getName(), RequestMapping.class.getSimpleName()
   1.189 -                        );
   1.190 -                    }
   1.191 -                }
   1.192 -            }
   1.193 -        } catch (SecurityException ex) {
   1.194 -            LOG.error("Scan for request mappings on declared methods failed.", ex);
   1.195 -        }
   1.196 -    }
   1.197 -
   1.198 -    @Override
   1.199 -    public void destroy() {
   1.200 -        mappings.clear();
   1.201 -        LOG.trace("{} destroyed", getServletName());
   1.202 -    }
   1.203 -
   1.204 -    /**
   1.205 -     * Sets the name of the content page.
   1.206 -     * <p>
   1.207 -     * It is sufficient to specify the name without any extension. The extension
   1.208 -     * is added automatically if not specified.
   1.209 -     *
   1.210 -     * @param req      the servlet request object
   1.211 -     * @param pageName the name of the content page
   1.212 -     * @see Constants#REQ_ATTR_CONTENT_PAGE
   1.213 -     */
   1.214 -    protected void setContentPage(HttpServletRequest req, String pageName) {
   1.215 -        req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName));
   1.216 -    }
   1.217 -
   1.218 -    /**
   1.219 -     * Sets the navigation menu.
   1.220 -     *
   1.221 -     * @param req     the servlet request object
   1.222 -     * @param jspName the name of the menu's jsp file
   1.223 -     * @see Constants#REQ_ATTR_NAVIGATION
   1.224 -     */
   1.225 -    protected void setNavigationMenu(HttpServletRequest req, String jspName) {
   1.226 -        req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName));
   1.227 -    }
   1.228 -
   1.229 -    /**
   1.230 -     * @param req      the servlet request object
   1.231 -     * @param location the location where to redirect
   1.232 -     * @see Constants#REQ_ATTR_REDIRECT_LOCATION
   1.233 -     */
   1.234 -    protected void setRedirectLocation(HttpServletRequest req, String location) {
   1.235 -        if (location.startsWith("./")) {
   1.236 -            location = location.replaceFirst("\\./", baseHref(req));
   1.237 -        }
   1.238 -        req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location);
   1.239 -    }
   1.240 -
   1.241 -    /**
   1.242 -     * Specifies the names of additional stylesheets used by this Servlet.
   1.243 -     * <p>
   1.244 -     * It is sufficient to specify the name without any extension. The extension
   1.245 -     * is added automatically if not specified.
   1.246 -     *
   1.247 -     * @param req         the servlet request object
   1.248 -     * @param stylesheets the names of the stylesheets
   1.249 -     */
   1.250 -    public void setStylesheet(HttpServletRequest req, String ... stylesheets) {
   1.251 -        req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays
   1.252 -                .stream(stylesheets)
   1.253 -                .map(s -> enforceExt(s, ".css"))
   1.254 -                .collect(Collectors.toUnmodifiableList()));
   1.255 -    }
   1.256 -
   1.257 -    /**
   1.258 -     * Sets the view model object.
   1.259 -     * The type must match the expected type in the JSP file.
   1.260 -     *
   1.261 -     * @param req       the servlet request object
   1.262 -     * @param viewModel the view model object
   1.263 -     */
   1.264 -    public void setViewModel(HttpServletRequest req, Object viewModel) {
   1.265 -        req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel);
   1.266 -    }
   1.267 -
   1.268 -    private <T> Optional<T> parseParameter(String paramValue, Class<T> clazz) {
   1.269 -        if (paramValue == null) return Optional.empty();
   1.270 -        if (clazz.equals(Boolean.class)) {
   1.271 -            if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) {
   1.272 -                return Optional.of((T) Boolean.FALSE);
   1.273 -            } else {
   1.274 -                return Optional.of((T) Boolean.TRUE);
   1.275 -            }
   1.276 -        }
   1.277 -        if (clazz.equals(String.class)) return Optional.of((T) paramValue);
   1.278 -        if (java.sql.Date.class.isAssignableFrom(clazz)) {
   1.279 -            try {
   1.280 -                return Optional.of((T) java.sql.Date.valueOf(paramValue));
   1.281 -            } catch (IllegalArgumentException ex) {
   1.282 -                return Optional.empty();
   1.283 -            }
   1.284 -        }
   1.285 -        try {
   1.286 -            final Constructor<T> ctor = clazz.getConstructor(String.class);
   1.287 -            return Optional.of(ctor.newInstance(paramValue));
   1.288 -        } catch (ReflectiveOperationException e) {
   1.289 -            // does not type check and is not convertible - treat as if the parameter was never set
   1.290 -            return Optional.empty();
   1.291 -        }
   1.292 -    }
   1.293 -
   1.294 -    /**
   1.295 -     * Obtains a request parameter of the specified type.
   1.296 -     * The specified type must have a single-argument constructor accepting a string to perform conversion.
   1.297 -     * The constructor of the specified type may throw an exception on conversion failures.
   1.298 -     *
   1.299 -     * @param req   the servlet request object
   1.300 -     * @param clazz the class object of the expected type
   1.301 -     * @param name  the name of the parameter
   1.302 -     * @param <T>   the expected type
   1.303 -     * @return the parameter value or an empty optional, if no parameter with the specified name was found
   1.304 -     */
   1.305 -    protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) {
   1.306 -        if (clazz.isArray()) {
   1.307 -            final String[] paramValues = req.getParameterValues(name);
   1.308 -            int len = paramValues == null ? 0 : paramValues.length;
   1.309 -            final var array = (T) Array.newInstance(clazz.getComponentType(), len);
   1.310 -            for (int i = 0; i < len; i++) {
   1.311 -                try {
   1.312 -                    final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class);
   1.313 -                    Array.set(array, i, ctor.newInstance(paramValues[i]));
   1.314 -                } catch (ReflectiveOperationException e) {
   1.315 -                    throw new RuntimeException(e);
   1.316 -                }
   1.317 -            }
   1.318 -            return Optional.of(array);
   1.319 -        } else {
   1.320 -            return parseParameter(req.getParameter(name), clazz);
   1.321 -        }
   1.322 -    }
   1.323 -
   1.324 -    /**
   1.325 -     * Tries to look up an entity with a key obtained from a request parameter.
   1.326 -     *
   1.327 -     * @param req   the servlet request object
   1.328 -     * @param clazz the class representing the type of the request parameter
   1.329 -     * @param name  the name of the request parameter
   1.330 -     * @param find  the find function (typically a DAO function)
   1.331 -     * @param <T>   the type of the request parameter
   1.332 -     * @param <R>   the type of the looked up entity
   1.333 -     * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing
   1.334 -     * @throws SQLException if the find function throws an exception
   1.335 -     */
   1.336 -    protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, Function<? super T, ? extends R> find) {
   1.337 -        final var param = getParameter(req, clazz, name);
   1.338 -        if (param.isPresent()) {
   1.339 -            return Optional.ofNullable(find.apply(param.get()));
   1.340 -        } else {
   1.341 -            return Optional.empty();
   1.342 -        }
   1.343 -    }
   1.344 -
   1.345 -    protected void setAttributeFromParameter(HttpServletRequest req, String name) {
   1.346 -        final var parm = req.getParameter(name);
   1.347 -        if (parm != null) {
   1.348 -            req.setAttribute(name, parm);
   1.349 -        }
   1.350 -    }
   1.351 -
   1.352 -    private String sanitizeRequestPath(HttpServletRequest req) {
   1.353 -        return Optional.ofNullable(req.getPathInfo()).orElse("/");
   1.354 -    }
   1.355 -
   1.356 -    private Optional<Map.Entry<PathPattern, Method>> findMapping(HttpMethod method, HttpServletRequest req) {
   1.357 -        return Optional.ofNullable(mappings.get(method)).flatMap(rm ->
   1.358 -                rm.entrySet().stream().filter(
   1.359 -                        kv -> kv.getKey().matches(sanitizeRequestPath(req))
   1.360 -                ).findAny()
   1.361 -        );
   1.362 -    }
   1.363 -
   1.364 -    protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   1.365 -        req.getRequestDispatcher(jspPath("site")).forward(req, resp);
   1.366 -    }
   1.367 -
   1.368 -    protected Optional<String[]> availableLanguages() {
   1.369 -        return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*"));
   1.370 -    }
   1.371 -
   1.372 -    private static String baseHref(HttpServletRequest req) {
   1.373 -        return String.format("%s://%s:%d%s/",
   1.374 -                req.getScheme(),
   1.375 -                req.getServerName(),
   1.376 -                req.getServerPort(),
   1.377 -                req.getContextPath());
   1.378 -    }
   1.379 -
   1.380 -    private static String enforceExt(String filename, String ext) {
   1.381 -        return filename.endsWith(ext) ? filename : filename + ext;
   1.382 -    }
   1.383 -
   1.384 -    private static String jspPath(String filename) {
   1.385 -        return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp");
   1.386 -    }
   1.387 -
   1.388 -    private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   1.389 -        // the very first thing to do is to force UTF-8
   1.390 -        req.setCharacterEncoding("UTF-8");
   1.391 -
   1.392 -        // choose the requested language as session language (if available) or fall back to english, otherwise
   1.393 -        HttpSession session = req.getSession();
   1.394 -        if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
   1.395 -            Optional<List<String>> availableLanguages = availableLanguages().map(Arrays::asList);
   1.396 -            Optional<Locale> reqLocale = Optional.of(req.getLocale());
   1.397 -            Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
   1.398 -            session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
   1.399 -            LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
   1.400 -        } else {
   1.401 -            Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
   1.402 -            resp.setLocale(sessionLocale);
   1.403 -            LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
   1.404 -        }
   1.405 -
   1.406 -        // set some internal request attributes
   1.407 -        final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse("");
   1.408 -        req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req));
   1.409 -        req.setAttribute(Constants.REQ_ATTR_PATH, fullPath);
   1.410 -
   1.411 -        // if this is an error path, bypass the normal flow
   1.412 -        if (fullPath.startsWith("/error/")) {
   1.413 -            final var mapping = findMapping(method, req);
   1.414 -            if (mapping.isPresent()) {
   1.415 -                invokeMapping(mapping.get(), req, resp, null);
   1.416 -            }
   1.417 -            return;
   1.418 -        }
   1.419 -
   1.420 -        // obtain a connection and create the data access objects
   1.421 -        final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
   1.422 -        final var ds = db.getDataSource();
   1.423 -        if (ds == null) {
   1.424 -            resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details.");
   1.425 -            return;
   1.426 -        }
   1.427 -        try (final var connection = ds.getConnection()) {
   1.428 -            final var dao = createDataAccessObjects(connection);
   1.429 -            try {
   1.430 -                connection.setAutoCommit(false);
   1.431 -                // call the handler, if available, or send an HTTP 404 error
   1.432 -                final var mapping = findMapping(method, req);
   1.433 -                if (mapping.isPresent()) {
   1.434 -                    invokeMapping(mapping.get(), req, resp, dao);
   1.435 -                } else {
   1.436 -                    resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   1.437 -                }
   1.438 -                connection.commit();
   1.439 -            } catch (SQLException ex) {
   1.440 -                LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage());
   1.441 -                LOG.debug("Details: ", ex);
   1.442 -                resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode());
   1.443 -                connection.rollback();
   1.444 -            }
   1.445 -        } catch (SQLException ex) {
   1.446 -            LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage());
   1.447 -            LOG.debug("Details: ", ex);
   1.448 -            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode());
   1.449 -        }
   1.450 -    }
   1.451 -
   1.452 -    @Override
   1.453 -    protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
   1.454 -            throws ServletException, IOException {
   1.455 -        doProcess(HttpMethod.GET, req, resp);
   1.456 -    }
   1.457 -
   1.458 -    @Override
   1.459 -    protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
   1.460 -            throws ServletException, IOException {
   1.461 -        doProcess(HttpMethod.POST, req, resp);
   1.462 -    }
   1.463 -}
     2.1 --- a/src/main/java/de/uapcore/lightpit/modules/ErrorModule.java	Sat Jan 23 14:47:59 2021 +0100
     2.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.3 @@ -1,61 +0,0 @@
     2.4 -/*
     2.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     2.6 - *
     2.7 - * Copyright 2021 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.modules;
    2.33 -
    2.34 -import de.uapcore.lightpit.AbstractServlet;
    2.35 -import de.uapcore.lightpit.HttpMethod;
    2.36 -import de.uapcore.lightpit.RequestMapping;
    2.37 -
    2.38 -import javax.servlet.ServletException;
    2.39 -import javax.servlet.annotation.WebServlet;
    2.40 -import javax.servlet.http.HttpServletRequest;
    2.41 -import javax.servlet.http.HttpServletResponse;
    2.42 -import java.io.IOException;
    2.43 -import java.util.Optional;
    2.44 -
    2.45 -@WebServlet(
    2.46 -        name = "ErrorModule",
    2.47 -        urlPatterns = "/error/*"
    2.48 -)
    2.49 -public final class ErrorModule extends AbstractServlet {
    2.50 -
    2.51 -    public static final String REQ_ATTR_RETURN_LINK = "returnLink";
    2.52 -
    2.53 -    @RequestMapping(requestPath = "generic", method = HttpMethod.GET)
    2.54 -    public void onError(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    2.55 -        Optional.ofNullable(req.getHeader("Referer")).ifPresent(
    2.56 -                referer -> req.setAttribute(REQ_ATTR_RETURN_LINK, referer)
    2.57 -        );
    2.58 -
    2.59 -        setStylesheet(req, "error");
    2.60 -        setContentPage(req, "error");
    2.61 -
    2.62 -        renderSite(req, resp);
    2.63 -    }
    2.64 -}
     3.1 --- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Sat Jan 23 14:47:59 2021 +0100
     3.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.3 @@ -1,113 +0,0 @@
     3.4 -/*
     3.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3.6 - *
     3.7 - * Copyright 2021 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.modules;
    3.33 -
    3.34 -import de.uapcore.lightpit.AbstractServlet;
    3.35 -import de.uapcore.lightpit.Constants;
    3.36 -import de.uapcore.lightpit.HttpMethod;
    3.37 -import de.uapcore.lightpit.RequestMapping;
    3.38 -import de.uapcore.lightpit.viewmodel.LanguageView;
    3.39 -import org.slf4j.Logger;
    3.40 -import org.slf4j.LoggerFactory;
    3.41 -
    3.42 -import javax.servlet.ServletException;
    3.43 -import javax.servlet.annotation.WebServlet;
    3.44 -import javax.servlet.http.HttpServletRequest;
    3.45 -import javax.servlet.http.HttpServletResponse;
    3.46 -import java.io.IOException;
    3.47 -import java.util.*;
    3.48 -
    3.49 -@WebServlet(
    3.50 -        name = "LanguageModule",
    3.51 -        urlPatterns = "/language/*"
    3.52 -)
    3.53 -public final class LanguageModule extends AbstractServlet {
    3.54 -
    3.55 -    private static final Logger LOG = LoggerFactory.getLogger(LanguageModule.class);
    3.56 -
    3.57 -    private final List<Locale> languages = new ArrayList<>();
    3.58 -
    3.59 -    @Override
    3.60 -    public void init() throws ServletException {
    3.61 -        super.init();
    3.62 -
    3.63 -        Optional<String[]> langs = availableLanguages();
    3.64 -        if (langs.isPresent()) {
    3.65 -            for (String lang : langs.get()) {
    3.66 -                try {
    3.67 -                    Locale locale = Locale.forLanguageTag(lang);
    3.68 -                    if (locale.getLanguage().isEmpty()) {
    3.69 -                        throw new IllformedLocaleException();
    3.70 -                    }
    3.71 -                    languages.add(locale);
    3.72 -                } catch (IllformedLocaleException ex) {
    3.73 -                    LOG.warn("Specified language {} in context parameter cannot be mapped to an existing locale - skipping.", lang);
    3.74 -                }
    3.75 -            }
    3.76 -
    3.77 -        } else {
    3.78 -            languages.add(Locale.ENGLISH);
    3.79 -            LOG.warn("Context parameter 'available-languages' not found. Only english will be available.");
    3.80 -        }
    3.81 -    }
    3.82 -
    3.83 -    @Override
    3.84 -    public void destroy() {
    3.85 -        super.destroy();
    3.86 -        languages.clear();
    3.87 -    }
    3.88 -
    3.89 -    @RequestMapping(method = HttpMethod.GET)
    3.90 -    public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    3.91 -
    3.92 -        final var viewModel = new LanguageView();
    3.93 -        viewModel.setLanguages(languages);
    3.94 -        viewModel.setBrowserLanguage(req.getLocale());
    3.95 -        viewModel.setCurrentLanguage((Locale)req.getSession().getAttribute(Constants.SESSION_ATTR_LANGUAGE));
    3.96 -
    3.97 -        setViewModel(req, viewModel);
    3.98 -        setStylesheet(req, "language");
    3.99 -        setContentPage(req, "language");
   3.100 -
   3.101 -        renderSite(req, resp);
   3.102 -    }
   3.103 -
   3.104 -    @RequestMapping(method = HttpMethod.POST)
   3.105 -    public void switchLanguage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   3.106 -
   3.107 -        Optional<Locale> chosenLanguage = Optional.ofNullable(req.getParameter("language"))
   3.108 -                .map(Locale::forLanguageTag)
   3.109 -                .filter((l) -> !l.getLanguage().isEmpty());
   3.110 -
   3.111 -        chosenLanguage.ifPresent((l) -> req.getSession().setAttribute(Constants.SESSION_ATTR_LANGUAGE, l));
   3.112 -        chosenLanguage.ifPresent(resp::setLocale);
   3.113 -
   3.114 -        handle(req, resp);
   3.115 -    }
   3.116 -}
     4.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat Jan 23 14:47:59 2021 +0100
     4.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.3 @@ -1,660 +0,0 @@
     4.4 -/*
     4.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     4.6 - *
     4.7 - * Copyright 2021 Mike Becker. All rights reserved.
     4.8 - *
     4.9 - * Redistribution and use in source and binary forms, with or without
    4.10 - * modification, are permitted provided that the following conditions are met:
    4.11 - *
    4.12 - *   1. Redistributions of source code must retain the above copyright
    4.13 - *      notice, this list of conditions and the following disclaimer.
    4.14 - *
    4.15 - *   2. Redistributions in binary form must reproduce the above copyright
    4.16 - *      notice, this list of conditions and the following disclaimer in the
    4.17 - *      documentation and/or other materials provided with the distribution.
    4.18 - *
    4.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    4.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    4.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    4.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    4.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    4.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    4.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    4.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    4.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    4.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    4.29 - * POSSIBILITY OF SUCH DAMAGE.
    4.30 - *
    4.31 - */
    4.32 -package de.uapcore.lightpit.modules;
    4.33 -
    4.34 -
    4.35 -import de.uapcore.lightpit.*;
    4.36 -import de.uapcore.lightpit.dao.DataAccessObject;
    4.37 -import de.uapcore.lightpit.entities.*;
    4.38 -import de.uapcore.lightpit.filter.AllFilter;
    4.39 -import de.uapcore.lightpit.filter.IssueFilter;
    4.40 -import de.uapcore.lightpit.filter.NoneFilter;
    4.41 -import de.uapcore.lightpit.filter.SpecificFilter;
    4.42 -import de.uapcore.lightpit.types.IssueCategory;
    4.43 -import de.uapcore.lightpit.types.IssueStatus;
    4.44 -import de.uapcore.lightpit.types.VersionStatus;
    4.45 -import de.uapcore.lightpit.types.WebColor;
    4.46 -import de.uapcore.lightpit.viewmodel.*;
    4.47 -import de.uapcore.lightpit.viewmodel.util.IssueSorter;
    4.48 -import org.slf4j.Logger;
    4.49 -import org.slf4j.LoggerFactory;
    4.50 -
    4.51 -import javax.servlet.ServletException;
    4.52 -import javax.servlet.annotation.WebServlet;
    4.53 -import javax.servlet.http.HttpServletRequest;
    4.54 -import javax.servlet.http.HttpServletResponse;
    4.55 -import java.io.IOException;
    4.56 -import java.sql.Date;
    4.57 -import java.sql.SQLException;
    4.58 -import java.util.NoSuchElementException;
    4.59 -import java.util.Optional;
    4.60 -import java.util.stream.Collectors;
    4.61 -import java.util.stream.Stream;
    4.62 -
    4.63 -@WebServlet(
    4.64 -        name = "ProjectsModule",
    4.65 -        urlPatterns = "/projects/*"
    4.66 -)
    4.67 -public final class ProjectsModule extends AbstractServlet {
    4.68 -
    4.69 -    private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
    4.70 -
    4.71 -    private static int parseIntOrZero(String str) {
    4.72 -        try {
    4.73 -            return Integer.parseInt(str);
    4.74 -        } catch (NumberFormatException ex) {
    4.75 -            return 0;
    4.76 -        }
    4.77 -    }
    4.78 -
    4.79 -    private void populate(ProjectView viewModel, PathParameters pathParameters, DataAccessObject dao) {
    4.80 -        dao.listProjects().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add);
    4.81 -
    4.82 -        if (pathParameters == null)
    4.83 -            return;
    4.84 -
    4.85 -        // Select Project
    4.86 -        final var project = dao.findProjectByNode(pathParameters.get("project"));
    4.87 -        if (project == null)
    4.88 -            return;
    4.89 -
    4.90 -        final var info = new ProjectInfo(project);
    4.91 -        info.setVersions(dao.listVersions(project));
    4.92 -        info.setComponents(dao.listComponents(project));
    4.93 -        info.setIssueSummary(dao.collectIssueSummary(project));
    4.94 -        viewModel.setProjectInfo(info);
    4.95 -
    4.96 -        // Select Version
    4.97 -        final var versionNode = pathParameters.get("version");
    4.98 -        if (versionNode != null) {
    4.99 -            if ("no-version".equals(versionNode)) {
   4.100 -                viewModel.setVersionFilter(ProjectView.NO_VERSION);
   4.101 -            } else if ("all-versions".equals(versionNode)) {
   4.102 -                viewModel.setVersionFilter(ProjectView.ALL_VERSIONS);
   4.103 -            } else {
   4.104 -                viewModel.setVersionFilter(dao.findVersionByNode(project, versionNode));
   4.105 -            }
   4.106 -        }
   4.107 -
   4.108 -        // Select Component
   4.109 -        final var componentNode = pathParameters.get("component");
   4.110 -        if (componentNode != null) {
   4.111 -            if ("no-component".equals(componentNode)) {
   4.112 -                viewModel.setComponentFilter(ProjectView.NO_COMPONENT);
   4.113 -            } else if ("all-components".equals(componentNode)) {
   4.114 -                viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS);
   4.115 -            } else {
   4.116 -                viewModel.setComponentFilter(dao.findComponentByNode(project, componentNode));
   4.117 -            }
   4.118 -        }
   4.119 -    }
   4.120 -
   4.121 -    private static String sanitizeNode(String node, String defaultValue) {
   4.122 -        String result = node == null || node.isBlank() ? defaultValue : node;
   4.123 -        result = result.replace('/', '-');
   4.124 -        if (result.equals(".") || result.equals("..")) {
   4.125 -            return "_"+result;
   4.126 -        } else {
   4.127 -            return result;
   4.128 -        }
   4.129 -    }
   4.130 -
   4.131 -    private void forwardView(HttpServletRequest req, HttpServletResponse resp, ProjectView viewModel, String name) throws ServletException, IOException {
   4.132 -        setViewModel(req, viewModel);
   4.133 -        setContentPage(req, name);
   4.134 -        setStylesheet(req, "projects");
   4.135 -        setNavigationMenu(req, "project-navmenu");
   4.136 -        renderSite(req, resp);
   4.137 -    }
   4.138 -
   4.139 -    @RequestMapping(method = HttpMethod.GET)
   4.140 -    public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException {
   4.141 -        final var viewModel = new ProjectView();
   4.142 -        populate(viewModel, null, dao);
   4.143 -
   4.144 -        for (var info : viewModel.getProjectList()) {
   4.145 -            info.setVersions(dao.listVersions(info.getProject()));
   4.146 -            info.setIssueSummary(dao.collectIssueSummary(info.getProject()));
   4.147 -        }
   4.148 -
   4.149 -        forwardView(req, resp, viewModel, "projects");
   4.150 -    }
   4.151 -
   4.152 -    private void configureProjectEditor(ProjectEditView viewModel, Project project, DataAccessObject dao) {
   4.153 -        viewModel.setProject(project);
   4.154 -        viewModel.setUsers(dao.listUsers());
   4.155 -    }
   4.156 -
   4.157 -    @RequestMapping(requestPath = "$project/edit", method = HttpMethod.GET)
   4.158 -    public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws IOException, SQLException, ServletException {
   4.159 -        final var viewModel = new ProjectEditView();
   4.160 -        populate(viewModel, pathParams, dao);
   4.161 -
   4.162 -        if (!viewModel.isProjectInfoPresent()) {
   4.163 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.164 -            return;
   4.165 -        }
   4.166 -
   4.167 -        configureProjectEditor(viewModel, viewModel.getProjectInfo().getProject(), dao);
   4.168 -        forwardView(req, resp, viewModel, "project-form");
   4.169 -    }
   4.170 -
   4.171 -    @RequestMapping(requestPath = "create", method = HttpMethod.GET)
   4.172 -    public void create(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException {
   4.173 -        final var viewModel = new ProjectEditView();
   4.174 -        populate(viewModel, null, dao);
   4.175 -        configureProjectEditor(viewModel, new Project(-1), dao);
   4.176 -        forwardView(req, resp, viewModel, "project-form");
   4.177 -    }
   4.178 -
   4.179 -    @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
   4.180 -    public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
   4.181 -
   4.182 -        try {
   4.183 -            final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
   4.184 -            project.setName(getParameter(req, String.class, "name").orElseThrow());
   4.185 -
   4.186 -            final var node = getParameter(req, String.class, "node").orElse(null);
   4.187 -            project.setNode(sanitizeNode(node, project.getName()));
   4.188 -            getParameter(req, Integer.class, "ordinal").ifPresent(project::setOrdinal);
   4.189 -
   4.190 -            getParameter(req, String.class, "description").ifPresent(project::setDescription);
   4.191 -            getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
   4.192 -            getParameter(req, Integer.class, "owner").map(
   4.193 -                    ownerId -> ownerId >= 0 ? new User(ownerId) : null
   4.194 -            ).ifPresent(project::setOwner);
   4.195 -
   4.196 -            if (project.getId() > 0) {
   4.197 -                dao.updateProject(project);
   4.198 -            } else {
   4.199 -                dao.insertProject(project);
   4.200 -            }
   4.201 -
   4.202 -            setRedirectLocation(req, "./projects/");
   4.203 -            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   4.204 -            LOG.debug("Successfully updated project {}", project.getName());
   4.205 -
   4.206 -            renderSite(req, resp);
   4.207 -        } catch (NoSuchElementException | IllegalArgumentException ex) {
   4.208 -            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
   4.209 -            // TODO: implement - fix issue #21
   4.210 -        }
   4.211 -    }
   4.212 -
   4.213 -    @RequestMapping(requestPath = "$project/$component/$version/issues/", method = HttpMethod.GET)
   4.214 -    public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws SQLException, IOException, ServletException {
   4.215 -        final var viewModel = new ProjectDetailsView();
   4.216 -        populate(viewModel, pathParams, dao);
   4.217 -
   4.218 -        if (!viewModel.isEveryFilterValid()) {
   4.219 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.220 -            return;
   4.221 -        }
   4.222 -
   4.223 -        final var project = viewModel.getProjectInfo().getProject();
   4.224 -        final var version = viewModel.getVersionFilter();
   4.225 -        final var component = viewModel.getComponentFilter();
   4.226 -
   4.227 -        // TODO: use new IssueFilter class for the ViewModel
   4.228 -
   4.229 -        final var projectFilter = new SpecificFilter<>(project);
   4.230 -        final IssueFilter filter;
   4.231 -        if (version.equals(ProjectView.NO_VERSION)) {
   4.232 -            if (component.equals(ProjectView.ALL_COMPONENTS)) {
   4.233 -                filter = new IssueFilter(projectFilter,
   4.234 -                        new NoneFilter<>(),
   4.235 -                        new AllFilter<>()
   4.236 -                );
   4.237 -            } else if (component.equals(ProjectView.NO_COMPONENT)) {
   4.238 -                filter = new IssueFilter(projectFilter,
   4.239 -                        new NoneFilter<>(),
   4.240 -                        new NoneFilter<>()
   4.241 -                );
   4.242 -            } else {
   4.243 -                filter = new IssueFilter(projectFilter,
   4.244 -                        new NoneFilter<>(),
   4.245 -                        new SpecificFilter<>(component)
   4.246 -                );
   4.247 -            }
   4.248 -        } else if (version.equals(ProjectView.ALL_VERSIONS)) {
   4.249 -            if (component.equals(ProjectView.ALL_COMPONENTS)) {
   4.250 -                filter = new IssueFilter(projectFilter,
   4.251 -                        new AllFilter<>(),
   4.252 -                        new AllFilter<>()
   4.253 -                );
   4.254 -            } else if (component.equals(ProjectView.NO_COMPONENT)) {
   4.255 -                filter = new IssueFilter(projectFilter,
   4.256 -                        new AllFilter<>(),
   4.257 -                        new NoneFilter<>()
   4.258 -                );
   4.259 -            } else {
   4.260 -                filter = new IssueFilter(projectFilter,
   4.261 -                        new AllFilter<>(),
   4.262 -                        new SpecificFilter<>(component)
   4.263 -                );
   4.264 -            }
   4.265 -        } else {
   4.266 -            if (component.equals(ProjectView.ALL_COMPONENTS)) {
   4.267 -                filter = new IssueFilter(projectFilter,
   4.268 -                        new SpecificFilter<>(version),
   4.269 -                        new AllFilter<>()
   4.270 -                );
   4.271 -            } else if (component.equals(ProjectView.NO_COMPONENT)) {
   4.272 -                filter = new IssueFilter(projectFilter,
   4.273 -                        new SpecificFilter<>(version),
   4.274 -                        new NoneFilter<>()
   4.275 -                );
   4.276 -            } else {
   4.277 -                filter = new IssueFilter(projectFilter,
   4.278 -                        new SpecificFilter<>(version),
   4.279 -                        new SpecificFilter<>(component)
   4.280 -                );
   4.281 -            }
   4.282 -        }
   4.283 -
   4.284 -        final var issues = dao.listIssues(filter);
   4.285 -        issues.sort(new IssueSorter(
   4.286 -                new IssueSorter.Criteria(IssueSorter.Field.DONE, true),
   4.287 -                new IssueSorter.Criteria(IssueSorter.Field.ETA, true),
   4.288 -                new IssueSorter.Criteria(IssueSorter.Field.UPDATED, false)
   4.289 -        ));
   4.290 -
   4.291 -
   4.292 -        viewModel.getProjectDetails().updateDetails(issues);
   4.293 -        if (version.getId() > 0)
   4.294 -            viewModel.getProjectDetails().updateVersionInfo(version);
   4.295 -
   4.296 -        forwardView(req, resp, viewModel, "project-details");
   4.297 -    }
   4.298 -
   4.299 -    @RequestMapping(requestPath = "$project/versions/", method = HttpMethod.GET)
   4.300 -    public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
   4.301 -        final var viewModel = new VersionsView();
   4.302 -        populate(viewModel, pathParameters, dao);
   4.303 -
   4.304 -        final var projectInfo = viewModel.getProjectInfo();
   4.305 -        if (projectInfo == null) {
   4.306 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.307 -            return;
   4.308 -        }
   4.309 -
   4.310 -        final var issues = dao.listIssues(
   4.311 -                new IssueFilter(
   4.312 -                        new SpecificFilter<>(projectInfo.getProject()),
   4.313 -                        new AllFilter<>(),
   4.314 -                        new AllFilter<>()
   4.315 -                )
   4.316 -        );
   4.317 -        viewModel.update(projectInfo.getVersions(), issues);
   4.318 -
   4.319 -        forwardView(req, resp, viewModel, "versions");
   4.320 -    }
   4.321 -
   4.322 -    @RequestMapping(requestPath = "$project/versions/$version/edit", method = HttpMethod.GET)
   4.323 -    public void editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
   4.324 -        final var viewModel = new VersionEditView();
   4.325 -        populate(viewModel, pathParameters, dao);
   4.326 -
   4.327 -        if (viewModel.getProjectInfo() == null || viewModel.getVersionFilter() == null) {
   4.328 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.329 -            return;
   4.330 -        }
   4.331 -
   4.332 -        viewModel.setVersion(viewModel.getVersionFilter());
   4.333 -
   4.334 -        forwardView(req, resp, viewModel, "version-form");
   4.335 -    }
   4.336 -
   4.337 -    @RequestMapping(requestPath = "$project/create-version", method = HttpMethod.GET)
   4.338 -    public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
   4.339 -        final var viewModel = new VersionEditView();
   4.340 -        populate(viewModel, pathParameters, dao);
   4.341 -
   4.342 -        if (viewModel.getProjectInfo() == null) {
   4.343 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.344 -            return;
   4.345 -        }
   4.346 -
   4.347 -        viewModel.setVersion(new Version(-1, viewModel.getProjectInfo().getProject().getId()));
   4.348 -
   4.349 -        forwardView(req, resp, viewModel, "version-form");
   4.350 -    }
   4.351 -
   4.352 -    @RequestMapping(requestPath = "commit-version", method = HttpMethod.POST)
   4.353 -    public void commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
   4.354 -
   4.355 -        try {
   4.356 -            final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow());
   4.357 -            if (project == null) {
   4.358 -                // TODO: improve error handling, because not found is not correct for this POST request
   4.359 -                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.360 -                return;
   4.361 -            }
   4.362 -            final var version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), project.getId());
   4.363 -            version.setName(getParameter(req, String.class, "name").orElseThrow());
   4.364 -
   4.365 -            final var node = getParameter(req, String.class, "node").orElse(null);
   4.366 -            version.setNode(sanitizeNode(node, version.getName()));
   4.367 -
   4.368 -            getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
   4.369 -            version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
   4.370 -
   4.371 -            if (version.getId() > 0) {
   4.372 -                dao.updateVersion(version);
   4.373 -            } else {
   4.374 -                dao.insertVersion(version);
   4.375 -            }
   4.376 -
   4.377 -            setRedirectLocation(req, "./projects/" + project.getNode() + "/versions/");
   4.378 -            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   4.379 -
   4.380 -            renderSite(req, resp);
   4.381 -        } catch (NoSuchElementException | IllegalArgumentException ex) {
   4.382 -            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
   4.383 -            // TODO: implement - fix issue #21
   4.384 -        }
   4.385 -    }
   4.386 -
   4.387 -    @RequestMapping(requestPath = "$project/components/", method = HttpMethod.GET)
   4.388 -    public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
   4.389 -        final var viewModel = new ComponentsView();
   4.390 -        populate(viewModel, pathParameters, dao);
   4.391 -
   4.392 -        final var projectInfo = viewModel.getProjectInfo();
   4.393 -        if (projectInfo == null) {
   4.394 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.395 -            return;
   4.396 -        }
   4.397 -
   4.398 -        final var issues = dao.listIssues(
   4.399 -                new IssueFilter(
   4.400 -                        new SpecificFilter<>(projectInfo.getProject()),
   4.401 -                        new AllFilter<>(),
   4.402 -                        new AllFilter<>()
   4.403 -                )
   4.404 -        );
   4.405 -        viewModel.update(projectInfo.getComponents(), issues);
   4.406 -
   4.407 -        forwardView(req, resp, viewModel, "components");
   4.408 -    }
   4.409 -
   4.410 -    @RequestMapping(requestPath = "$project/components/$component/edit", method = HttpMethod.GET)
   4.411 -    public void editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
   4.412 -        final var viewModel = new ComponentEditView();
   4.413 -        populate(viewModel, pathParameters, dao);
   4.414 -
   4.415 -        if (viewModel.getProjectInfo() == null || viewModel.getComponentFilter() == null) {
   4.416 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.417 -            return;
   4.418 -        }
   4.419 -
   4.420 -        viewModel.setComponent(viewModel.getComponentFilter());
   4.421 -        viewModel.setUsers(dao.listUsers());
   4.422 -
   4.423 -        forwardView(req, resp, viewModel, "component-form");
   4.424 -    }
   4.425 -
   4.426 -    @RequestMapping(requestPath = "$project/create-component", method = HttpMethod.GET)
   4.427 -    public void createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
   4.428 -        final var viewModel = new ComponentEditView();
   4.429 -        populate(viewModel, pathParameters, dao);
   4.430 -
   4.431 -        if (viewModel.getProjectInfo() == null) {
   4.432 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.433 -            return;
   4.434 -        }
   4.435 -
   4.436 -        viewModel.setComponent(new Component(-1, viewModel.getProjectInfo().getProject().getId()));
   4.437 -        viewModel.setUsers(dao.listUsers());
   4.438 -
   4.439 -        forwardView(req, resp, viewModel, "component-form");
   4.440 -    }
   4.441 -
   4.442 -    @RequestMapping(requestPath = "commit-component", method = HttpMethod.POST)
   4.443 -    public void commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
   4.444 -
   4.445 -        try {
   4.446 -            final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow());
   4.447 -            if (project == null) {
   4.448 -                // TODO: improve error handling, because not found is not correct for this POST request
   4.449 -                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.450 -                return;
   4.451 -            }
   4.452 -            final var component = new Component(getParameter(req, Integer.class, "id").orElseThrow(), project.getId());
   4.453 -            component.setName(getParameter(req, String.class, "name").orElseThrow());
   4.454 -
   4.455 -            final var node = getParameter(req, String.class, "node").orElse(null);
   4.456 -            component.setNode(sanitizeNode(node, component.getName()));
   4.457 -
   4.458 -            component.setColor(getParameter(req, WebColor.class, "color").orElseThrow());
   4.459 -            getParameter(req, Integer.class, "ordinal").ifPresent(component::setOrdinal);
   4.460 -            getParameter(req, Integer.class, "lead").map(
   4.461 -                    userid -> userid >= 0 ? new User(userid) : null
   4.462 -            ).ifPresent(component::setLead);
   4.463 -            getParameter(req, String.class, "description").ifPresent(component::setDescription);
   4.464 -
   4.465 -            if (component.getId() > 0) {
   4.466 -                dao.updateComponent(component);
   4.467 -            } else {
   4.468 -                dao.insertComponent(component);
   4.469 -            }
   4.470 -
   4.471 -            setRedirectLocation(req, "./projects/" + project.getNode() + "/components/");
   4.472 -            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   4.473 -
   4.474 -            renderSite(req, resp);
   4.475 -        } catch (NoSuchElementException | IllegalArgumentException ex) {
   4.476 -            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
   4.477 -            // TODO: implement - fix issue #21
   4.478 -        }
   4.479 -    }
   4.480 -
   4.481 -    private void configureIssueEditor(IssueEditView viewModel, Issue issue, DataAccessObject dao) {
   4.482 -        final var project = viewModel.getProjectInfo().getProject();
   4.483 -        issue.setProject(project); // automatically set current project for new issues
   4.484 -        viewModel.setIssue(issue);
   4.485 -        viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
   4.486 -        viewModel.setUsers(dao.listUsers());
   4.487 -        viewModel.setComponents(dao.listComponents(project));
   4.488 -    }
   4.489 -
   4.490 -    @RequestMapping(requestPath = "$project/issues/$issue/view", method = HttpMethod.GET)
   4.491 -    public void viewIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
   4.492 -        final var viewModel = new IssueDetailView();
   4.493 -        populate(viewModel, pathParameters, dao);
   4.494 -
   4.495 -        final var projectInfo = viewModel.getProjectInfo();
   4.496 -        if (projectInfo == null) {
   4.497 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.498 -            return;
   4.499 -        }
   4.500 -
   4.501 -        final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue")));
   4.502 -        if (issue == null) {
   4.503 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.504 -            return;
   4.505 -        }
   4.506 -
   4.507 -        viewModel.setIssue(issue);
   4.508 -        viewModel.setComments(dao.listComments(issue));
   4.509 -
   4.510 -        viewModel.processMarkdown();
   4.511 -
   4.512 -        forwardView(req, resp, viewModel, "issue-view");
   4.513 -    }
   4.514 -
   4.515 -    // TODO: why should the issue editor be child of $project?
   4.516 -    @RequestMapping(requestPath = "$project/issues/$issue/edit", method = HttpMethod.GET)
   4.517 -    public void editIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
   4.518 -        final var viewModel = new IssueEditView();
   4.519 -        populate(viewModel, pathParameters, dao);
   4.520 -
   4.521 -        final var projectInfo = viewModel.getProjectInfo();
   4.522 -        if (projectInfo == null) {
   4.523 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.524 -            return;
   4.525 -        }
   4.526 -
   4.527 -        final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue")));
   4.528 -        if (issue == null) {
   4.529 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.530 -            return;
   4.531 -        }
   4.532 -
   4.533 -        configureIssueEditor(viewModel, issue, dao);
   4.534 -
   4.535 -        forwardView(req, resp, viewModel, "issue-form");
   4.536 -    }
   4.537 -
   4.538 -    @RequestMapping(requestPath = "$project/create-issue", method = HttpMethod.GET)
   4.539 -    public void createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
   4.540 -        final var viewModel = new IssueEditView();
   4.541 -        populate(viewModel, pathParameters, dao);
   4.542 -
   4.543 -        final var projectInfo = viewModel.getProjectInfo();
   4.544 -        if (projectInfo == null) {
   4.545 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.546 -            return;
   4.547 -        }
   4.548 -
   4.549 -        setAttributeFromParameter(req, "more");
   4.550 -        setAttributeFromParameter(req, "cid");
   4.551 -        setAttributeFromParameter(req, "vid");
   4.552 -
   4.553 -        final var issue = new Issue(-1, projectInfo.getProject(), null);
   4.554 -        issue.setProject(projectInfo.getProject());
   4.555 -        configureIssueEditor(viewModel, issue, dao);
   4.556 -
   4.557 -        forwardView(req, resp, viewModel, "issue-form");
   4.558 -    }
   4.559 -
   4.560 -    @RequestMapping(requestPath = "commit-issue", method = HttpMethod.POST)
   4.561 -    public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
   4.562 -        try {
   4.563 -            final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow());
   4.564 -            if (project == null) {
   4.565 -                // TODO: improve error handling, because not found is not correct for this POST request
   4.566 -                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.567 -                return;
   4.568 -            }
   4.569 -            final var componentId = getParameter(req, Integer.class, "component");
   4.570 -            final Component component;
   4.571 -            if (componentId.isPresent()) {
   4.572 -                component = dao.findComponent(componentId.get());
   4.573 -            } else {
   4.574 -                component = null;
   4.575 -            }
   4.576 -            final var issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), project, component);
   4.577 -            getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
   4.578 -            getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
   4.579 -            issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
   4.580 -            getParameter(req, Integer.class, "assignee").map(userid -> {
   4.581 -                if (userid >= 0) {
   4.582 -                    return new User(userid);
   4.583 -                } else if (userid == -2) {
   4.584 -                    return Optional.ofNullable(component).map(Component::getLead).orElse(null);
   4.585 -                } else {
   4.586 -                    return null;
   4.587 -                }
   4.588 -            }
   4.589 -            ).ifPresent(issue::setAssignee);
   4.590 -            getParameter(req, String.class, "description").ifPresent(issue::setDescription);
   4.591 -            getParameter(req, Date.class, "eta").ifPresent(issue::setEta);
   4.592 -
   4.593 -            getParameter(req, Integer[].class, "affected")
   4.594 -                    .map(Stream::of)
   4.595 -                    .map(stream ->
   4.596 -                            stream.map(id -> new Version(id, project.getId()))
   4.597 -                                    .collect(Collectors.toList())
   4.598 -                    ).ifPresent(issue::setAffectedVersions);
   4.599 -            getParameter(req, Integer[].class, "resolved")
   4.600 -                    .map(Stream::of)
   4.601 -                    .map(stream ->
   4.602 -                            stream.map(id -> new Version(id, project.getId()))
   4.603 -                                    .collect(Collectors.toList())
   4.604 -                    ).ifPresent(issue::setResolvedVersions);
   4.605 -
   4.606 -            if (issue.getId() > 0) {
   4.607 -                dao.updateIssue(issue);
   4.608 -            } else {
   4.609 -                dao.insertIssue(issue);
   4.610 -            }
   4.611 -
   4.612 -            if (getParameter(req, Boolean.class, "create-another").orElse(false)) {
   4.613 -                // TODO: fix #38 - automatically select component (and version)
   4.614 -                setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/create-issue?more=true");
   4.615 -            } else{
   4.616 -                setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/issues/" + issue.getId() + "/view");
   4.617 -            }
   4.618 -            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   4.619 -
   4.620 -            renderSite(req, resp);
   4.621 -        } catch (NoSuchElementException | IllegalArgumentException ex) {
   4.622 -            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
   4.623 -            // TODO: implement - fix issue #21
   4.624 -        }
   4.625 -    }
   4.626 -
   4.627 -    @RequestMapping(requestPath = "commit-issue-comment", method = HttpMethod.POST)
   4.628 -    public void commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
   4.629 -        final var issueIdParam = getParameter(req, Integer.class, "issueid");
   4.630 -        if (issueIdParam.isEmpty()) {
   4.631 -            resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form.");
   4.632 -            return;
   4.633 -        }
   4.634 -        final var issue = dao.findIssue(issueIdParam.get());
   4.635 -        if (issue == null) {
   4.636 -            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   4.637 -            return;
   4.638 -        }
   4.639 -        try {
   4.640 -            final var issueComment = new IssueComment(getParameter(req, Integer.class, "commentid").orElse(-1), issue.getId());
   4.641 -            issueComment.setComment(getParameter(req, String.class, "comment").orElse(""));
   4.642 -
   4.643 -            if (issueComment.getComment().isBlank()) {
   4.644 -                throw new IllegalArgumentException("comment.null");
   4.645 -            }
   4.646 -
   4.647 -            LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId());
   4.648 -            if (req.getRemoteUser() != null) {
   4.649 -                Optional.ofNullable(dao.findUserByName(req.getRemoteUser())).ifPresent(issueComment::setAuthor);
   4.650 -            }
   4.651 -
   4.652 -            dao.insertComment(issueComment);
   4.653 -
   4.654 -            setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/view");
   4.655 -            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   4.656 -
   4.657 -            renderSite(req, resp);
   4.658 -        } catch (NoSuchElementException | IllegalArgumentException ex) {
   4.659 -            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
   4.660 -            // TODO: implement - fix issue #21
   4.661 -        }
   4.662 -    }
   4.663 -}
     5.1 --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Sat Jan 23 14:47:59 2021 +0100
     5.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.3 @@ -1,113 +0,0 @@
     5.4 -/*
     5.5 - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     5.6 - *
     5.7 - * Copyright 2021 Mike Becker. All rights reserved.
     5.8 - *
     5.9 - * Redistribution and use in source and binary forms, with or without
    5.10 - * modification, are permitted provided that the following conditions are met:
    5.11 - *
    5.12 - *   1. Redistributions of source code must retain the above copyright
    5.13 - *      notice, this list of conditions and the following disclaimer.
    5.14 - *
    5.15 - *   2. Redistributions in binary form must reproduce the above copyright
    5.16 - *      notice, this list of conditions and the following disclaimer in the
    5.17 - *      documentation and/or other materials provided with the distribution.
    5.18 - *
    5.19 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    5.20 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    5.21 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    5.22 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    5.23 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    5.24 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    5.25 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    5.26 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    5.27 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    5.28 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    5.29 - * POSSIBILITY OF SUCH DAMAGE.
    5.30 - *
    5.31 - */
    5.32 -package de.uapcore.lightpit.modules;
    5.33 -
    5.34 -import de.uapcore.lightpit.AbstractServlet;
    5.35 -import de.uapcore.lightpit.Constants;
    5.36 -import de.uapcore.lightpit.HttpMethod;
    5.37 -import de.uapcore.lightpit.RequestMapping;
    5.38 -import de.uapcore.lightpit.dao.DataAccessObject;
    5.39 -import de.uapcore.lightpit.entities.User;
    5.40 -import de.uapcore.lightpit.viewmodel.UsersEditView;
    5.41 -import de.uapcore.lightpit.viewmodel.UsersView;
    5.42 -import org.slf4j.Logger;
    5.43 -import org.slf4j.LoggerFactory;
    5.44 -
    5.45 -import javax.servlet.ServletException;
    5.46 -import javax.servlet.annotation.WebServlet;
    5.47 -import javax.servlet.http.HttpServletRequest;
    5.48 -import javax.servlet.http.HttpServletResponse;
    5.49 -import java.io.IOException;
    5.50 -import java.sql.SQLException;
    5.51 -import java.util.NoSuchElementException;
    5.52 -
    5.53 -@WebServlet(
    5.54 -        name = "UsersModule",
    5.55 -        urlPatterns = "/teams/*"
    5.56 -)
    5.57 -public final class UsersModule extends AbstractServlet {
    5.58 -
    5.59 -    private static final Logger LOG = LoggerFactory.getLogger(UsersModule.class);
    5.60 -
    5.61 -    @RequestMapping(method = HttpMethod.GET)
    5.62 -    public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException {
    5.63 -        final var viewModel = new UsersView();
    5.64 -        viewModel.setUsers(dao.listUsers());
    5.65 -        setViewModel(req, viewModel);
    5.66 -        setContentPage(req, "users");
    5.67 -
    5.68 -        renderSite(req, resp);
    5.69 -    }
    5.70 -
    5.71 -    @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
    5.72 -    public void edit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException {
    5.73 -
    5.74 -        final var viewModel = new UsersEditView();
    5.75 -        viewModel.setUser(findByParameter(req, Integer.class, "id", dao::findUser).orElse(new User(-1)));
    5.76 -
    5.77 -        setViewModel(req, viewModel);
    5.78 -        setContentPage(req, "user-form");
    5.79 -
    5.80 -        renderSite(req, resp);
    5.81 -    }
    5.82 -
    5.83 -    @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
    5.84 -    public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException {
    5.85 -
    5.86 -        User user = new User(-1);
    5.87 -        try {
    5.88 -            user = new User(getParameter(req, Integer.class, "userid").orElseThrow());
    5.89 -            user.setUsername(getParameter(req, String.class, "username").orElseThrow());
    5.90 -            getParameter(req, String.class, "givenname").ifPresent(user::setGivenname);
    5.91 -            getParameter(req, String.class, "lastname").ifPresent(user::setLastname);
    5.92 -            getParameter(req, String.class, "mail").ifPresent(user::setMail);
    5.93 -
    5.94 -            if (user.getId() > 0) {
    5.95 -                dao.updateUser(user);
    5.96 -            } else {
    5.97 -                dao.insertUser(user);
    5.98 -            }
    5.99 -
   5.100 -            setRedirectLocation(req, "./teams/");
   5.101 -            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   5.102 -
   5.103 -            LOG.debug("Successfully updated user {}", user.getUsername());
   5.104 -        } catch (NoSuchElementException | IllegalArgumentException ex) {
   5.105 -            final var viewModel = new UsersEditView();
   5.106 -            viewModel.setUser(user);
   5.107 -            // TODO: viewModel.setErrorText()
   5.108 -            setViewModel(req, viewModel);
   5.109 -            setContentPage(req, "user-form");
   5.110 -            LOG.warn("Form validation failure: {}", ex.getMessage());
   5.111 -            LOG.debug("Details:", ex);
   5.112 -        }
   5.113 -
   5.114 -        renderSite(req, resp);
   5.115 -    }
   5.116 -}
     6.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ComponentEditView.java	Sat Jan 23 14:47:59 2021 +0100
     6.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.3 @@ -1,40 +0,0 @@
     6.4 -package de.uapcore.lightpit.viewmodel;
     6.5 -
     6.6 -import de.uapcore.lightpit.entities.Component;
     6.7 -import de.uapcore.lightpit.entities.User;
     6.8 -
     6.9 -import java.util.List;
    6.10 -
    6.11 -public class ComponentEditView extends ProjectView {
    6.12 -    private Component component;
    6.13 -    private List<User> users;
    6.14 -    private String errorText;
    6.15 -
    6.16 -    public ComponentEditView() {
    6.17 -        setSelectedPage(SELECTED_PAGE_COMPONENTS);
    6.18 -    }
    6.19 -
    6.20 -    public void setComponent(Component component) {
    6.21 -        this.component = component;
    6.22 -    }
    6.23 -
    6.24 -    public Component getComponent() {
    6.25 -        return component;
    6.26 -    }
    6.27 -
    6.28 -    public List<User> getUsers() {
    6.29 -        return users;
    6.30 -    }
    6.31 -
    6.32 -    public void setUsers(List<User> users) {
    6.33 -        this.users = users;
    6.34 -    }
    6.35 -
    6.36 -    public String getErrorText() {
    6.37 -        return errorText;
    6.38 -    }
    6.39 -
    6.40 -    public void setErrorText(String errorText) {
    6.41 -        this.errorText = errorText;
    6.42 -    }
    6.43 -}
     7.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ComponentInfo.java	Sat Jan 23 14:47:59 2021 +0100
     7.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.3 @@ -1,42 +0,0 @@
     7.4 -package de.uapcore.lightpit.viewmodel;
     7.5 -
     7.6 -import de.uapcore.lightpit.entities.Component;
     7.7 -import de.uapcore.lightpit.entities.Issue;
     7.8 -import de.uapcore.lightpit.entities.IssueSummary;
     7.9 -
    7.10 -import java.util.ArrayList;
    7.11 -import java.util.List;
    7.12 -
    7.13 -public class ComponentInfo {
    7.14 -
    7.15 -    private final Component component;
    7.16 -
    7.17 -    private final IssueSummary issueSummary = new IssueSummary();
    7.18 -
    7.19 -    private final List<Issue> issues = new ArrayList<>();
    7.20 -
    7.21 -    public ComponentInfo(Component component) {
    7.22 -        this.component = component;
    7.23 -    }
    7.24 -
    7.25 -    public Component getComponent() {
    7.26 -        return component;
    7.27 -    }
    7.28 -
    7.29 -    public IssueSummary getIssueSummary() {
    7.30 -        return issueSummary;
    7.31 -    }
    7.32 -
    7.33 -    public List<Issue> getIssues() {
    7.34 -        return issues;
    7.35 -    }
    7.36 -
    7.37 -    public void collectIssues(List<Issue> issues) {
    7.38 -        for (Issue issue : issues) {
    7.39 -            if (component.equals(issue.getComponent())) {
    7.40 -                this.issues.add(issue);
    7.41 -                this.issueSummary.add(issue);
    7.42 -            }
    7.43 -        }
    7.44 -    }
    7.45 -}
     8.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ComponentsView.java	Sat Jan 23 14:47:59 2021 +0100
     8.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.3 @@ -1,29 +0,0 @@
     8.4 -package de.uapcore.lightpit.viewmodel;
     8.5 -
     8.6 -import de.uapcore.lightpit.entities.Component;
     8.7 -import de.uapcore.lightpit.entities.Issue;
     8.8 -
     8.9 -import java.util.ArrayList;
    8.10 -import java.util.List;
    8.11 -
    8.12 -public class ComponentsView extends ProjectView {
    8.13 -
    8.14 -    private List<ComponentInfo> componentInfos = new ArrayList<>();
    8.15 -
    8.16 -    public ComponentsView() {
    8.17 -        setSelectedPage(SELECTED_PAGE_COMPONENTS);
    8.18 -    }
    8.19 -
    8.20 -    public void update(List<Component> components, List<Issue> issues) {
    8.21 -        componentInfos.clear();
    8.22 -        for (var component : components) {
    8.23 -            final var info = new ComponentInfo(component);
    8.24 -            info.collectIssues(issues);
    8.25 -            componentInfos.add(info);
    8.26 -        }
    8.27 -    }
    8.28 -
    8.29 -    public List<ComponentInfo> getComponentInfos() {
    8.30 -        return componentInfos;
    8.31 -    }
    8.32 -}
     9.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueDetailView.java	Sat Jan 23 14:47:59 2021 +0100
     9.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.3 @@ -1,47 +0,0 @@
     9.4 -package de.uapcore.lightpit.viewmodel;
     9.5 -
     9.6 -import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
     9.7 -import com.vladsch.flexmark.ext.tables.TablesExtension;
     9.8 -import com.vladsch.flexmark.html.HtmlRenderer;
     9.9 -import com.vladsch.flexmark.parser.Parser;
    9.10 -import com.vladsch.flexmark.util.data.MutableDataSet;
    9.11 -import de.uapcore.lightpit.entities.Issue;
    9.12 -import de.uapcore.lightpit.entities.IssueComment;
    9.13 -
    9.14 -import java.util.Arrays;
    9.15 -import java.util.List;
    9.16 -
    9.17 -public class IssueDetailView extends ProjectView {
    9.18 -    private Issue issue;
    9.19 -
    9.20 -    private List<IssueComment> comments;
    9.21 -
    9.22 -    public void setIssue(Issue issue) {
    9.23 -        this.issue = issue;
    9.24 -    }
    9.25 -
    9.26 -    public Issue getIssue() {
    9.27 -        return issue;
    9.28 -    }
    9.29 -
    9.30 -    public List<IssueComment> getComments() {
    9.31 -        return comments;
    9.32 -    }
    9.33 -
    9.34 -    public void setComments(List<IssueComment> comments) {
    9.35 -        this.comments = comments;
    9.36 -    }
    9.37 -
    9.38 -    public void processMarkdown() {
    9.39 -        final var options = new MutableDataSet()
    9.40 -                .set(Parser.EXTENSIONS, Arrays.asList(TablesExtension.create(), StrikethroughExtension.create()))
    9.41 -                .toImmutable();
    9.42 -        final var parser = Parser.builder(options).build();
    9.43 -        final var renderer = HtmlRenderer.builder(options).build();
    9.44 -
    9.45 -        issue.setDescription(renderer.render(parser.parse(issue.getDescription())));
    9.46 -        for (var comment : comments) {
    9.47 -            comment.setComment(renderer.render(parser.parse(comment.getComment())));
    9.48 -        }
    9.49 -    }
    9.50 -}
    10.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Sat Jan 23 14:47:59 2021 +0100
    10.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.3 @@ -1,75 +0,0 @@
    10.4 -package de.uapcore.lightpit.viewmodel;
    10.5 -
    10.6 -import de.uapcore.lightpit.entities.Component;
    10.7 -import de.uapcore.lightpit.entities.Project;
    10.8 -import de.uapcore.lightpit.entities.User;
    10.9 -import de.uapcore.lightpit.entities.Version;
   10.10 -import de.uapcore.lightpit.types.IssueCategory;
   10.11 -import de.uapcore.lightpit.types.IssueStatus;
   10.12 -import de.uapcore.lightpit.types.VersionStatus;
   10.13 -
   10.14 -import java.util.*;
   10.15 -
   10.16 -public class IssueEditView extends IssueDetailView {
   10.17 -    private List<Project> projects = Collections.emptyList();
   10.18 -    private Set<Version> versionsUpcoming = new HashSet<>();
   10.19 -    private Set<Version> versionsRecent = new HashSet<>();
   10.20 -    private List<User> users;
   10.21 -    private List<Component> components;
   10.22 -
   10.23 -    public List<Project> getProjects() {
   10.24 -        return projects;
   10.25 -    }
   10.26 -
   10.27 -    public void setProjects(List<Project> projects) {
   10.28 -        this.projects = projects;
   10.29 -    }
   10.30 -
   10.31 -    public Collection<Version> getVersionsUpcoming() {
   10.32 -        return versionsUpcoming;
   10.33 -    }
   10.34 -
   10.35 -    public Collection<Version> getVersionsRecent() {
   10.36 -        return versionsRecent;
   10.37 -    }
   10.38 -
   10.39 -    public void configureVersionSelectors(List<Version> versions) {
   10.40 -        versionsRecent.clear();
   10.41 -        versionsUpcoming.clear();
   10.42 -        // keep the current selection, if any
   10.43 -        versionsRecent.addAll(getIssue().getAffectedVersions());
   10.44 -        versionsUpcoming.addAll(getIssue().getResolvedVersions());
   10.45 -        for (var v : versions) {
   10.46 -            if (v.getStatus().isReleased()) {
   10.47 -                if (!v.getStatus().equals(VersionStatus.Deprecated))
   10.48 -                    versionsRecent.add(v);
   10.49 -            } else {
   10.50 -                versionsUpcoming.add(v);
   10.51 -            }
   10.52 -        }
   10.53 -    }
   10.54 -
   10.55 -    public List<User> getUsers() {
   10.56 -        return users;
   10.57 -    }
   10.58 -
   10.59 -    public void setUsers(List<User> users) {
   10.60 -        this.users = users;
   10.61 -    }
   10.62 -
   10.63 -    public List<Component> getComponents() {
   10.64 -        return components;
   10.65 -    }
   10.66 -
   10.67 -    public void setComponents(List<Component> components) {
   10.68 -        this.components = components;
   10.69 -    }
   10.70 -
   10.71 -    public IssueStatus[] getIssueStatus() {
   10.72 -        return IssueStatus.values();
   10.73 -    }
   10.74 -
   10.75 -    public IssueCategory[] getIssueCategory() {
   10.76 -        return IssueCategory.values();
   10.77 -    }
   10.78 -}
    11.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java	Sat Jan 23 14:47:59 2021 +0100
    11.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    11.3 @@ -1,37 +0,0 @@
    11.4 -package de.uapcore.lightpit.viewmodel;
    11.5 -
    11.6 -import de.uapcore.lightpit.entities.Issue;
    11.7 -import de.uapcore.lightpit.entities.Project;
    11.8 -import de.uapcore.lightpit.entities.Version;
    11.9 -
   11.10 -import java.util.List;
   11.11 -
   11.12 -public class IssuesView {
   11.13 -    private List<Issue> issues;
   11.14 -    private Project project;
   11.15 -    private Version version;
   11.16 -
   11.17 -    public List<Issue> getIssues() {
   11.18 -        return issues;
   11.19 -    }
   11.20 -
   11.21 -    public void setIssues(List<Issue> issues) {
   11.22 -        this.issues = issues;
   11.23 -    }
   11.24 -
   11.25 -    public Version getVersion() {
   11.26 -        return version;
   11.27 -    }
   11.28 -
   11.29 -    public void setVersion(Version version) {
   11.30 -        this.version = version;
   11.31 -    }
   11.32 -
   11.33 -    public Project getProject() {
   11.34 -        return project;
   11.35 -    }
   11.36 -
   11.37 -    public void setProject(Project project) {
   11.38 -        this.project = project;
   11.39 -    }
   11.40 -}
    12.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java	Sat Jan 23 14:47:59 2021 +0100
    12.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.3 @@ -1,36 +0,0 @@
    12.4 -package de.uapcore.lightpit.viewmodel;
    12.5 -
    12.6 -import java.util.List;
    12.7 -import java.util.Locale;
    12.8 -
    12.9 -public class LanguageView {
   12.10 -
   12.11 -    private List<Locale> languages;
   12.12 -    private Locale browserLanguage;
   12.13 -    private Locale currentLanguage;
   12.14 -
   12.15 -
   12.16 -    public List<Locale> getLanguages() {
   12.17 -        return languages;
   12.18 -    }
   12.19 -
   12.20 -    public void setLanguages(List<Locale> languages) {
   12.21 -        this.languages = languages;
   12.22 -    }
   12.23 -
   12.24 -    public Locale getBrowserLanguage() {
   12.25 -        return browserLanguage;
   12.26 -    }
   12.27 -
   12.28 -    public void setBrowserLanguage(Locale browserLanguage) {
   12.29 -        this.browserLanguage = browserLanguage;
   12.30 -    }
   12.31 -
   12.32 -    public Locale getCurrentLanguage() {
   12.33 -        return currentLanguage;
   12.34 -    }
   12.35 -
   12.36 -    public void setCurrentLanguage(Locale currentLanguage) {
   12.37 -        this.currentLanguage = currentLanguage;
   12.38 -    }
   12.39 -}
    13.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java	Sat Jan 23 14:47:59 2021 +0100
    13.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.3 @@ -1,38 +0,0 @@
    13.4 -package de.uapcore.lightpit.viewmodel;
    13.5 -
    13.6 -import de.uapcore.lightpit.entities.Issue;
    13.7 -import de.uapcore.lightpit.entities.IssueSummary;
    13.8 -import de.uapcore.lightpit.entities.Version;
    13.9 -
   13.10 -import java.util.List;
   13.11 -
   13.12 -public class ProjectDetails {
   13.13 -
   13.14 -    private VersionInfo versionInfo = null;
   13.15 -
   13.16 -    private List<Issue> issues;
   13.17 -    private IssueSummary issueSummary;
   13.18 -
   13.19 -    public void updateDetails(List<Issue> issues) {
   13.20 -        this.issues = issues;
   13.21 -        issueSummary = new IssueSummary();
   13.22 -        issues.forEach(issueSummary::add);
   13.23 -    }
   13.24 -
   13.25 -    public void updateVersionInfo(Version version) {
   13.26 -        versionInfo = new VersionInfo(version);
   13.27 -        versionInfo.collectIssues(issues);
   13.28 -    }
   13.29 -
   13.30 -    public List<Issue> getIssues() {
   13.31 -        return issues;
   13.32 -    }
   13.33 -
   13.34 -    public IssueSummary getIssueSummary() {
   13.35 -        return issueSummary;
   13.36 -    }
   13.37 -
   13.38 -    public VersionInfo getVersionInfo() {
   13.39 -        return versionInfo;
   13.40 -    }
   13.41 -}
    14.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetailsView.java	Sat Jan 23 14:47:59 2021 +0100
    14.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    14.3 @@ -1,10 +0,0 @@
    14.4 -package de.uapcore.lightpit.viewmodel;
    14.5 -
    14.6 -public class ProjectDetailsView extends ProjectView {
    14.7 -
    14.8 -    private final ProjectDetails projectDetails = new ProjectDetails();
    14.9 -
   14.10 -    public ProjectDetails getProjectDetails() {
   14.11 -        return projectDetails;
   14.12 -    }
   14.13 -}
    15.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java	Sat Jan 23 14:47:59 2021 +0100
    15.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    15.3 @@ -1,37 +0,0 @@
    15.4 -package de.uapcore.lightpit.viewmodel;
    15.5 -
    15.6 -import de.uapcore.lightpit.entities.Project;
    15.7 -import de.uapcore.lightpit.entities.User;
    15.8 -
    15.9 -import java.util.List;
   15.10 -
   15.11 -public class ProjectEditView extends ProjectView {
   15.12 -
   15.13 -    private Project project;
   15.14 -    private List<User> users;
   15.15 -    private String errorText;
   15.16 -
   15.17 -    public Project getProject() {
   15.18 -        return project;
   15.19 -    }
   15.20 -
   15.21 -    public void setProject(Project project) {
   15.22 -        this.project = project;
   15.23 -    }
   15.24 -
   15.25 -    public List<User> getUsers() {
   15.26 -        return users;
   15.27 -    }
   15.28 -
   15.29 -    public void setUsers(List<User> users) {
   15.30 -        this.users = users;
   15.31 -    }
   15.32 -
   15.33 -    public String getErrorText() {
   15.34 -        return errorText;
   15.35 -    }
   15.36 -
   15.37 -    public void setErrorText(String errorText) {
   15.38 -        this.errorText = errorText;
   15.39 -    }
   15.40 -}
    16.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java	Sat Jan 23 14:47:59 2021 +0100
    16.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    16.3 @@ -1,69 +0,0 @@
    16.4 -package de.uapcore.lightpit.viewmodel;
    16.5 -
    16.6 -import de.uapcore.lightpit.entities.Component;
    16.7 -import de.uapcore.lightpit.entities.IssueSummary;
    16.8 -import de.uapcore.lightpit.entities.Project;
    16.9 -import de.uapcore.lightpit.entities.Version;
   16.10 -
   16.11 -import java.util.Collections;
   16.12 -import java.util.List;
   16.13 -
   16.14 -public class ProjectInfo {
   16.15 -
   16.16 -    private final Project project;
   16.17 -    private List<Version> versions = Collections.emptyList();
   16.18 -    private List<Component> components = Collections.emptyList();
   16.19 -    private IssueSummary issueSummary = new IssueSummary();
   16.20 -
   16.21 -    public ProjectInfo(Project project) {
   16.22 -        this.project = project;
   16.23 -    }
   16.24 -
   16.25 -    public Project getProject() {
   16.26 -        return project;
   16.27 -    }
   16.28 -
   16.29 -    public List<Version> getVersions() {
   16.30 -        return versions;
   16.31 -    }
   16.32 -
   16.33 -    public void setVersions(List<Version> versions) {
   16.34 -        this.versions = versions;
   16.35 -    }
   16.36 -
   16.37 -    public List<Component> getComponents() {
   16.38 -        return components;
   16.39 -    }
   16.40 -
   16.41 -    public void setComponents(List<Component> components) {
   16.42 -        this.components = components;
   16.43 -    }
   16.44 -
   16.45 -    public Version getLatestVersion() {
   16.46 -        // expects versions to be sorted by status descending
   16.47 -        for (var v : versions) {
   16.48 -            if (v.getStatus().isReleased())
   16.49 -                return v;
   16.50 -        }
   16.51 -        return null;
   16.52 -    }
   16.53 -
   16.54 -    public Version getNextVersion() {
   16.55 -        // expects versions to be sorted by status descending
   16.56 -        Version next = null;
   16.57 -        for (var v : versions) {
   16.58 -            if (v.getStatus().isReleased())
   16.59 -                break;
   16.60 -            next = v;
   16.61 -        }
   16.62 -        return next;
   16.63 -    }
   16.64 -
   16.65 -    public IssueSummary getIssueSummary() {
   16.66 -        return issueSummary;
   16.67 -    }
   16.68 -
   16.69 -    public void setIssueSummary(IssueSummary issueSummary) {
   16.70 -        this.issueSummary = issueSummary;
   16.71 -    }
   16.72 -}
    17.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Sat Jan 23 14:47:59 2021 +0100
    17.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    17.3 @@ -1,79 +0,0 @@
    17.4 -package de.uapcore.lightpit.viewmodel;
    17.5 -
    17.6 -import de.uapcore.lightpit.entities.Component;
    17.7 -import de.uapcore.lightpit.entities.Version;
    17.8 -
    17.9 -import java.util.ArrayList;
   17.10 -import java.util.List;
   17.11 -
   17.12 -public class ProjectView {
   17.13 -
   17.14 -    public static final int SELECTED_PAGE_ISSUES = 0;
   17.15 -    public static final int SELECTED_PAGE_VERSIONS = 1;
   17.16 -    public static final int SELECTED_PAGE_COMPONENTS = 2;
   17.17 -
   17.18 -    // TODO: use new Filter class
   17.19 -
   17.20 -    public static final Version ALL_VERSIONS = new Version(0,0);
   17.21 -    public static final Version NO_VERSION = new Version(-1,0);
   17.22 -    public static final Component ALL_COMPONENTS = new Component(0,0);
   17.23 -    public static final Component NO_COMPONENT = new Component(-1,0);
   17.24 -
   17.25 -    static {
   17.26 -        ALL_VERSIONS.setNode("all-versions");
   17.27 -        NO_VERSION.setNode("no-version");
   17.28 -        ALL_COMPONENTS.setNode("all-components");
   17.29 -        NO_COMPONENT.setNode("no-component");
   17.30 -    }
   17.31 -
   17.32 -    private final List<ProjectInfo> projectList = new ArrayList<>();
   17.33 -    private ProjectInfo projectInfo;
   17.34 -    private Version versionFilter;
   17.35 -    private Component componentFilter;
   17.36 -
   17.37 -    private int selectedPage = SELECTED_PAGE_ISSUES;
   17.38 -
   17.39 -    public List<ProjectInfo> getProjectList() {
   17.40 -        return projectList;
   17.41 -    }
   17.42 -
   17.43 -    public ProjectInfo getProjectInfo() {
   17.44 -        return projectInfo;
   17.45 -    }
   17.46 -
   17.47 -    public void setProjectInfo(ProjectInfo projectInfo) {
   17.48 -        this.projectInfo = projectInfo;
   17.49 -    }
   17.50 -
   17.51 -    public int getSelectedPage() {
   17.52 -        return selectedPage;
   17.53 -    }
   17.54 -
   17.55 -    public void setSelectedPage(int selectedPage) {
   17.56 -        this.selectedPage = selectedPage;
   17.57 -    }
   17.58 -
   17.59 -    public Version getVersionFilter() {
   17.60 -        return versionFilter;
   17.61 -    }
   17.62 -
   17.63 -    public void setVersionFilter(Version versionFilter) {
   17.64 -        this.versionFilter = versionFilter;
   17.65 -    }
   17.66 -
   17.67 -    public Component getComponentFilter() {
   17.68 -        return componentFilter;
   17.69 -    }
   17.70 -
   17.71 -    public void setComponentFilter(Component componentFilter) {
   17.72 -        this.componentFilter = componentFilter;
   17.73 -    }
   17.74 -
   17.75 -    public boolean isProjectInfoPresent() {
   17.76 -        return projectInfo != null;
   17.77 -    }
   17.78 -
   17.79 -    public boolean isEveryFilterValid() {
   17.80 -        return projectInfo != null && versionFilter != null && componentFilter != null;
   17.81 -    }
   17.82 -}
    18.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java	Sat Jan 23 14:47:59 2021 +0100
    18.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.3 @@ -1,24 +0,0 @@
    18.4 -package de.uapcore.lightpit.viewmodel;
    18.5 -
    18.6 -import de.uapcore.lightpit.entities.User;
    18.7 -
    18.8 -public class UsersEditView {
    18.9 -    private User user;
   18.10 -    private String errorText;
   18.11 -
   18.12 -    public User getUser() {
   18.13 -        return user;
   18.14 -    }
   18.15 -
   18.16 -    public void setUser(User user) {
   18.17 -        this.user = user;
   18.18 -    }
   18.19 -
   18.20 -    public String getErrorText() {
   18.21 -        return errorText;
   18.22 -    }
   18.23 -
   18.24 -    public void setErrorText(String errorText) {
   18.25 -        this.errorText = errorText;
   18.26 -    }
   18.27 -}
    19.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java	Sat Jan 23 14:47:59 2021 +0100
    19.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.3 @@ -1,17 +0,0 @@
    19.4 -package de.uapcore.lightpit.viewmodel;
    19.5 -
    19.6 -import de.uapcore.lightpit.entities.User;
    19.7 -
    19.8 -import java.util.List;
    19.9 -
   19.10 -public class UsersView {
   19.11 -    private List<User> users;
   19.12 -
   19.13 -    public List<User> getUsers() {
   19.14 -        return users;
   19.15 -    }
   19.16 -
   19.17 -    public void setUsers(List<User> users) {
   19.18 -        this.users = users;
   19.19 -    }
   19.20 -}
    20.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Sat Jan 23 14:47:59 2021 +0100
    20.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    20.3 @@ -1,33 +0,0 @@
    20.4 -package de.uapcore.lightpit.viewmodel;
    20.5 -
    20.6 -import de.uapcore.lightpit.entities.Version;
    20.7 -import de.uapcore.lightpit.types.VersionStatus;
    20.8 -
    20.9 -public class VersionEditView extends ProjectView {
   20.10 -    private Version version;
   20.11 -    private String errorText;
   20.12 -
   20.13 -    public VersionEditView() {
   20.14 -        setSelectedPage(SELECTED_PAGE_VERSIONS);
   20.15 -    }
   20.16 -
   20.17 -    public void setVersion(Version version) {
   20.18 -        this.version = version;
   20.19 -    }
   20.20 -
   20.21 -    public Version getVersion() {
   20.22 -        return version;
   20.23 -    }
   20.24 -
   20.25 -    public VersionStatus[] getVersionStatus() {
   20.26 -        return VersionStatus.values();
   20.27 -    }
   20.28 -
   20.29 -    public String getErrorText() {
   20.30 -        return errorText;
   20.31 -    }
   20.32 -
   20.33 -    public void setErrorText(String errorText) {
   20.34 -        this.errorText = errorText;
   20.35 -    }
   20.36 -}
    21.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java	Sat Jan 23 14:47:59 2021 +0100
    21.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    21.3 @@ -1,64 +0,0 @@
    21.4 -package de.uapcore.lightpit.viewmodel;
    21.5 -
    21.6 -import de.uapcore.lightpit.entities.Issue;
    21.7 -import de.uapcore.lightpit.entities.IssueSummary;
    21.8 -import de.uapcore.lightpit.entities.Version;
    21.9 -
   21.10 -import java.util.ArrayList;
   21.11 -import java.util.List;
   21.12 -
   21.13 -public class VersionInfo {
   21.14 -
   21.15 -    private final Version version;
   21.16 -
   21.17 -    private final IssueSummary reportedTotal = new IssueSummary();
   21.18 -    private final IssueSummary resolvedTotal = new IssueSummary();
   21.19 -
   21.20 -    private final List<Issue> reported = new ArrayList<>();
   21.21 -    private final List<Issue> resolved = new ArrayList<>();
   21.22 -
   21.23 -    public VersionInfo(Version version) {
   21.24 -        this.version = version;
   21.25 -    }
   21.26 -
   21.27 -    public Version getVersion() {
   21.28 -        return version;
   21.29 -    }
   21.30 -
   21.31 -    public void addReported(Issue issue) {
   21.32 -        reportedTotal.add(issue);
   21.33 -        reported.add(issue);
   21.34 -    }
   21.35 -
   21.36 -    public void addResolved(Issue issue) {
   21.37 -        resolvedTotal.add(issue);
   21.38 -        resolved.add(issue);
   21.39 -    }
   21.40 -
   21.41 -    public IssueSummary getReportedTotal() {
   21.42 -        return reportedTotal;
   21.43 -    }
   21.44 -
   21.45 -    public IssueSummary getResolvedTotal() {
   21.46 -        return resolvedTotal;
   21.47 -    }
   21.48 -
   21.49 -    public List<Issue> getReported() {
   21.50 -        return reported;
   21.51 -    }
   21.52 -
   21.53 -    public List<Issue> getResolved() {
   21.54 -        return resolved;
   21.55 -    }
   21.56 -
   21.57 -    public void collectIssues(List<Issue> issues) {
   21.58 -        for (Issue issue : issues) {
   21.59 -            if (issue.getAffectedVersions().contains(version)) {
   21.60 -                addReported(issue);
   21.61 -            }
   21.62 -            if (issue.getResolvedVersions().contains(version)) {
   21.63 -                addResolved(issue);
   21.64 -            }
   21.65 -        }
   21.66 -    }
   21.67 -}
    22.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java	Sat Jan 23 14:47:59 2021 +0100
    22.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    22.3 @@ -1,29 +0,0 @@
    22.4 -package de.uapcore.lightpit.viewmodel;
    22.5 -
    22.6 -import de.uapcore.lightpit.entities.Issue;
    22.7 -import de.uapcore.lightpit.entities.Version;
    22.8 -
    22.9 -import java.util.ArrayList;
   22.10 -import java.util.List;
   22.11 -
   22.12 -public class VersionsView extends ProjectView {
   22.13 -
   22.14 -    private List<VersionInfo> versionInfos = new ArrayList<>();
   22.15 -
   22.16 -    public VersionsView() {
   22.17 -        setSelectedPage(SELECTED_PAGE_VERSIONS);
   22.18 -    }
   22.19 -
   22.20 -    public void update(List<Version> versions, List<Issue> issues) {
   22.21 -        versionInfos.clear();
   22.22 -        for (var version : versions) {
   22.23 -            final var info = new VersionInfo(version);
   22.24 -            info.collectIssues(issues);
   22.25 -            versionInfos.add(info);
   22.26 -        }
   22.27 -    }
   22.28 -
   22.29 -    public List<VersionInfo> getVersionInfos() {
   22.30 -        return versionInfos;
   22.31 -    }
   22.32 -}
    23.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java	Sat Jan 23 14:47:59 2021 +0100
    23.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    23.3 @@ -1,83 +0,0 @@
    23.4 -package de.uapcore.lightpit.viewmodel.util;
    23.5 -
    23.6 -import de.uapcore.lightpit.entities.Issue;
    23.7 -import de.uapcore.lightpit.types.IssueStatusPhase;
    23.8 -
    23.9 -import java.util.Arrays;
   23.10 -import java.util.Comparator;
   23.11 -
   23.12 -public class IssueSorter implements Comparator<Issue> {
   23.13 -
   23.14 -    public enum Field {
   23.15 -        DONE, ETA, UPDATED
   23.16 -    }
   23.17 -
   23.18 -    public static class Criteria {
   23.19 -        private Field field;
   23.20 -        private boolean asc;
   23.21 -
   23.22 -        public Criteria(Field field, boolean asc) {
   23.23 -            this.field = field;
   23.24 -            this.asc = asc;
   23.25 -        }
   23.26 -
   23.27 -        @Override
   23.28 -        public boolean equals(Object obj) {
   23.29 -            if (obj == null || !obj.getClass().equals(Criteria.class))
   23.30 -                return false;
   23.31 -            final var other = (Criteria)obj;
   23.32 -            return other.field.equals(field) && other.asc == asc;
   23.33 -        }
   23.34 -    }
   23.35 -
   23.36 -    private final Criteria[] criteria;
   23.37 -
   23.38 -    public IssueSorter(Criteria ... criteria) {
   23.39 -        this.criteria = criteria;
   23.40 -    }
   23.41 -
   23.42 -    private int compare(Issue left, Issue right, Criteria criteria) {
   23.43 -        if (left.equals(right))
   23.44 -            return 0;
   23.45 -
   23.46 -        int result;
   23.47 -        switch (criteria.field) {
   23.48 -            case DONE:
   23.49 -                result = Boolean.compare(
   23.50 -                        left.getStatus().getPhase().equals(IssueStatusPhase.Companion.getDone()),
   23.51 -                        right.getStatus().getPhase().equals(IssueStatusPhase.Companion.getDone()));
   23.52 -                break;
   23.53 -            case ETA:
   23.54 -                if (left.getEta() != null && right.getEta() != null)
   23.55 -                    result = left.getEta().compareTo(right.getEta());
   23.56 -                else if (left.getEta() == null && right.getEta() == null)
   23.57 -                    result = 0;
   23.58 -                else
   23.59 -                    result = left.getEta() != null ? -1 : 1;
   23.60 -                break;
   23.61 -            case UPDATED:
   23.62 -                result = left.getUpdated().compareTo(right.getUpdated());
   23.63 -                break;
   23.64 -            default:
   23.65 -                throw new UnsupportedOperationException();
   23.66 -        }
   23.67 -        return criteria.asc ? result : -result;
   23.68 -    }
   23.69 -
   23.70 -    @Override
   23.71 -    public int compare(Issue left, Issue right) {
   23.72 -        for (var c : criteria) {
   23.73 -            int r = compare(left, right, c);
   23.74 -            if (r != 0) return r;
   23.75 -        }
   23.76 -        return 0;
   23.77 -    }
   23.78 -
   23.79 -    @Override
   23.80 -    public boolean equals(Object o) {
   23.81 -        if (o == null || !o.getClass().equals(IssueSorter.class))
   23.82 -            return false;
   23.83 -        final var other = (IssueSorter) o;
   23.84 -        return Arrays.equals(criteria, other.criteria);
   23.85 -    }
   23.86 -}
    24.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    24.2 +++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt	Fri Apr 02 11:59:14 2021 +0200
    24.3 @@ -0,0 +1,182 @@
    24.4 +/*
    24.5 + * Copyright 2021 Mike Becker. All rights reserved.
    24.6 + *
    24.7 + * Redistribution and use in source and binary forms, with or without
    24.8 + * modification, are permitted provided that the following conditions are met:
    24.9 + *
   24.10 + * 1. Redistributions of source code must retain the above copyright
   24.11 + * notice, this list of conditions and the following disclaimer.
   24.12 + *
   24.13 + * 2. Redistributions in binary form must reproduce the above copyright
   24.14 + * notice, this list of conditions and the following disclaimer in the
   24.15 + * documentation and/or other materials provided with the distribution.
   24.16 + *
   24.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   24.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   24.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   24.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   24.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   24.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   24.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   24.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   24.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   24.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   24.27 + */
   24.28 +
   24.29 +package de.uapcore.lightpit
   24.30 +
   24.31 +import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME
   24.32 +import de.uapcore.lightpit.dao.DataAccessObject
   24.33 +import de.uapcore.lightpit.dao.createDataAccessObject
   24.34 +import java.sql.SQLException
   24.35 +import java.util.*
   24.36 +import javax.servlet.http.HttpServlet
   24.37 +import javax.servlet.http.HttpServletRequest
   24.38 +import javax.servlet.http.HttpServletResponse
   24.39 +
   24.40 +abstract class AbstractServlet : LoggingTrait, HttpServlet() {
   24.41 +
   24.42 +    /**
   24.43 +     * Contains the GET request mappings.
   24.44 +     */
   24.45 +    private val getMappings = mutableMapOf<PathPattern, MappingMethod>()
   24.46 +
   24.47 +    /**
   24.48 +     * Contains the POST request mappings.
   24.49 +     */
   24.50 +    private val postMappings = mutableMapOf<PathPattern, MappingMethod>()
   24.51 +
   24.52 +    protected fun get(pattern: String, method: MappingMethod) {
   24.53 +        getMappings[PathPattern(pattern)] = method
   24.54 +    }
   24.55 +
   24.56 +    protected fun post(pattern: String, method: MappingMethod) {
   24.57 +        postMappings[PathPattern(pattern)] = method
   24.58 +    }
   24.59 +
   24.60 +    private fun notFound(http: HttpRequest, dao: DataAccessObject) {
   24.61 +        http.response.sendError(HttpServletResponse.SC_NOT_FOUND)
   24.62 +    }
   24.63 +
   24.64 +    private fun findMapping(
   24.65 +        mappings: Map<PathPattern, MappingMethod>,
   24.66 +        req: HttpServletRequest
   24.67 +    ): Pair<PathPattern, MappingMethod> {
   24.68 +        val requestPath = sanitizedRequestPath(req)
   24.69 +        val candidates = mappings.filter { it.key.matches(requestPath) }
   24.70 +        return if (candidates.isEmpty()) {
   24.71 +            Pair(PathPattern(requestPath), ::notFound)
   24.72 +        } else {
   24.73 +            if (candidates.size > 1) {
   24.74 +                logger().warn("Ambiguous mapping for request path '{}'", requestPath)
   24.75 +            }
   24.76 +            candidates.entries.first().toPair()
   24.77 +        }
   24.78 +    }
   24.79 +
   24.80 +    private fun invokeMapping(
   24.81 +        mapping: Pair<PathPattern, MappingMethod>,
   24.82 +        req: HttpServletRequest,
   24.83 +        resp: HttpServletResponse,
   24.84 +        dao: DataAccessObject
   24.85 +    ) {
   24.86 +        val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req))
   24.87 +        val method = mapping.second
   24.88 +        logger().trace("invoke {}", method)
   24.89 +        method(HttpRequest(req, resp, params), dao)
   24.90 +    }
   24.91 +
   24.92 +    private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/"
   24.93 +
   24.94 +    private fun doProcess(
   24.95 +        req: HttpServletRequest,
   24.96 +        resp: HttpServletResponse,
   24.97 +        mappings: Map<PathPattern, MappingMethod>
   24.98 +    ) {
   24.99 +        val session = req.session
  24.100 +
  24.101 +        // the very first thing to do is to force UTF-8
  24.102 +        req.characterEncoding = "UTF-8"
  24.103 +
  24.104 +        // choose the requested language as session language (if available) or fall back to english, otherwise
  24.105 +        if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
  24.106 +            val availableLanguages = availableLanguages()
  24.107 +            val reqLocale = req.locale
  24.108 +            val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first()
  24.109 +            session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale)
  24.110 +            logger().debug(
  24.111 +                "Setting language for new session {}: {}", session.id, sessionLocale.displayLanguage
  24.112 +            )
  24.113 +        } else {
  24.114 +            val sessionLocale = session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale
  24.115 +            resp.locale = sessionLocale
  24.116 +            logger().trace("Continuing session {} with language {}", session.id, sessionLocale)
  24.117 +        }
  24.118 +
  24.119 +        // set some internal request attributes
  24.120 +        val http = HttpRequest(req, resp)
  24.121 +        val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("")
  24.122 +        req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref)
  24.123 +        req.setAttribute(Constants.REQ_ATTR_PATH, fullPath)
  24.124 +        req.getHeader("Referer")?.let {
  24.125 +            // TODO: add a sanity check to avoid link injection
  24.126 +            req.setAttribute(Constants.REQ_ATTR_REFERER, it)
  24.127 +        }
  24.128 +
  24.129 +        // if this is an error path, bypass the normal flow
  24.130 +        if (fullPath.startsWith("/error/")) {
  24.131 +            http.styleSheets = listOf("error")
  24.132 +            http.render("error")
  24.133 +            return
  24.134 +        }
  24.135 +
  24.136 +        // obtain a connection and create the data access objects
  24.137 +        val dsp = req.servletContext.getAttribute(SC_ATTR_NAME) as DataSourceProvider
  24.138 +        val dialect = dsp.dialect
  24.139 +        val ds = dsp.dataSource
  24.140 +        if (ds == null) {
  24.141 +            resp.sendError(
  24.142 +                HttpServletResponse.SC_SERVICE_UNAVAILABLE,
  24.143 +                "JNDI DataSource lookup failed. See log for details."
  24.144 +            )
  24.145 +            return
  24.146 +        }
  24.147 +        try {
  24.148 +            ds.connection.use { connection ->
  24.149 +                val dao = createDataAccessObject(dialect, connection)
  24.150 +                try {
  24.151 +                    connection.autoCommit = false
  24.152 +                    invokeMapping(findMapping(mappings, req), req, resp, dao)
  24.153 +                    connection.commit()
  24.154 +                } catch (ex: SQLException) {
  24.155 +                    logger().warn("Database transaction failed (Code {}): {}", ex.errorCode, ex.message)
  24.156 +                    logger().debug("Details: ", ex)
  24.157 +                    resp.sendError(
  24.158 +                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
  24.159 +                        "Unhandled Transaction Error - Code: " + ex.errorCode
  24.160 +                    )
  24.161 +                    connection.rollback()
  24.162 +                }
  24.163 +            }
  24.164 +        } catch (ex: SQLException) {
  24.165 +            logger().error("Severe Database Exception (Code {}): {}", ex.errorCode, ex.message)
  24.166 +            logger().debug("Details: ", ex)
  24.167 +            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode)
  24.168 +        }
  24.169 +    }
  24.170 +
  24.171 +    override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  24.172 +        doProcess(req, resp, getMappings)
  24.173 +    }
  24.174 +
  24.175 +    override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
  24.176 +        doProcess(req, resp, postMappings)
  24.177 +    }
  24.178 +
  24.179 +    protected fun availableLanguages(): List<Locale> {
  24.180 +        val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList()
  24.181 +        val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() }
  24.182 +        return if (locales.isEmpty()) listOf(Locale.ENGLISH) else locales
  24.183 +    }
  24.184 +
  24.185 +}
    25.1 --- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Sat Jan 23 14:47:59 2021 +0100
    25.2 +++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Fri Apr 02 11:59:14 2021 +0200
    25.3 @@ -52,7 +52,7 @@
    25.4      const val CTX_ATTR_DB_DIALECT = "db-dialect"
    25.5  
    25.6      /**
    25.7 -     * Key for the request attribute containing the optional navigation menu jsp.
    25.8 +     * Key for the request attribute containing the optional navigation menu.
    25.9       */
   25.10      const val REQ_ATTR_NAVIGATION = "navMenu"
   25.11  
   25.12 @@ -88,6 +88,11 @@
   25.13      const val REQ_ATTR_REDIRECT_LOCATION = "redirectLocation"
   25.14  
   25.15      /**
   25.16 +     * Key for the optional return link based on the referer header.
   25.17 +     */
   25.18 +    const val REQ_ATTR_REFERER = "returnLink"
   25.19 +
   25.20 +    /**
   25.21       * Key for the current language selection within the session.
   25.22       */
   25.23      const val SESSION_ATTR_LANGUAGE = "language"
    26.1 --- a/src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt	Sat Jan 23 14:47:59 2021 +0100
    26.2 +++ b/src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt	Fri Apr 02 11:59:14 2021 +0200
    26.3 @@ -61,12 +61,12 @@
    26.4          /**
    26.5           * The attribute name in the Servlet context under which an instance of this class can be found.
    26.6           */
    26.7 -        val SC_ATTR_NAME = "lightpit.service.DataSourceProvider"
    26.8 +        const val SC_ATTR_NAME = "lightpit.service.DataSourceProvider"
    26.9  
   26.10          /**
   26.11           * Timeout in seconds for the validation test.
   26.12           */
   26.13 -        private val DB_TEST_TIMEOUT = 10
   26.14 +        private const val DB_TEST_TIMEOUT = 10
   26.15  
   26.16          /**
   26.17           * The default schema to test against when validating the connection.
   26.18 @@ -74,12 +74,12 @@
   26.19           *
   26.20           * @see Constants.CTX_ATTR_DB_SCHEMA
   26.21           */
   26.22 -        private val DB_DEFAULT_SCHEMA = "lightpit"
   26.23 +        private const val DB_DEFAULT_SCHEMA = "lightpit"
   26.24  
   26.25          /**
   26.26           * The JNDI resource name for the data source.
   26.27           */
   26.28 -        private val DS_JNDI_NAME = "jdbc/lightpit/app"
   26.29 +        private const val DS_JNDI_NAME = "jdbc/lightpit/app"
   26.30      }
   26.31  
   26.32      private fun checkConnection(ds: DataSource, testSchema: String) {
    27.1 --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Sat Jan 23 14:47:59 2021 +0100
    27.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Fri Apr 02 11:59:14 2021 +0200
    27.3 @@ -25,45 +25,106 @@
    27.4  
    27.5  package de.uapcore.lightpit
    27.6  
    27.7 +import de.uapcore.lightpit.dao.DataAccessObject
    27.8 +import de.uapcore.lightpit.viewmodel.NavMenu
    27.9 +import de.uapcore.lightpit.viewmodel.View
   27.10 +import javax.servlet.http.HttpServletRequest
   27.11 +import javax.servlet.http.HttpServletResponse
   27.12 +import javax.servlet.http.HttpSession
   27.13  import kotlin.math.min
   27.14  
   27.15 -/**
   27.16 - * Maps requests to methods.
   27.17 - *
   27.18 - * This annotation is used to annotate methods within classes which
   27.19 - * override [AbstractServlet].
   27.20 - */
   27.21 -@MustBeDocumented
   27.22 -@Retention(AnnotationRetention.RUNTIME)
   27.23 -@Target(AnnotationTarget.FUNCTION)
   27.24 -annotation class RequestMapping(
   27.25 +typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
   27.26 +typealias PathParameters = Map<String, String>
   27.27 +
   27.28 +class HttpRequest(
   27.29 +    val request: HttpServletRequest,
   27.30 +    val response: HttpServletResponse,
   27.31 +    val pathParams: PathParameters = emptyMap()
   27.32 +) {
   27.33 +    val session: HttpSession = request.session
   27.34 +
   27.35 +    val remoteUser: String? = request.remoteUser
   27.36  
   27.37      /**
   27.38 -     * Specifies the HTTP method.
   27.39 +     * The name of the content page.
   27.40       *
   27.41 -     * @return the HTTP method handled by the annotated Java method
   27.42 +     * @see Constants#REQ_ATTR_CONTENT_PAGE
   27.43       */
   27.44 -    val method: HttpMethod,
   27.45 +    var contentPage = ""
   27.46 +        set(value) {
   27.47 +            field = value
   27.48 +            request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
   27.49 +        }
   27.50  
   27.51      /**
   27.52 -     * Specifies the request path relative to the module path.
   27.53 -     * The trailing slash is important.
   27.54 -     * A node may start with a dollar ($) sign.
   27.55 -     * This part of the path is then treated as an path parameter.
   27.56 -     * Path parameters can be obtained by including the [PathParameters] type in the signature.
   27.57 +     * A list of additional style sheets.
   27.58       *
   27.59 -     * @return the request path the annotated method should handle
   27.60 +     * @see Constants#REQ_ATTR_STYLESHEET
   27.61       */
   27.62 -    val requestPath: String = "/"
   27.63 -)
   27.64 +    var styleSheets = emptyList<String>()
   27.65 +        set(value) {
   27.66 +            field = value
   27.67 +            request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
   27.68 +                value.map { it.withExt(".css") }
   27.69 +            )
   27.70 +        }
   27.71  
   27.72 -class PathParameters : HashMap<String, String>()
   27.73 +    /**
   27.74 +     * The name of the navigation menu JSP.
   27.75 +     *
   27.76 +     * @see Constants#REQ_ATTR_NAVIGATION
   27.77 +     */
   27.78 +    var navigationMenu: NavMenu? = null
   27.79 +        set(value) {
   27.80 +            field = value
   27.81 +            request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
   27.82 +        }
   27.83 +
   27.84 +    var redirectLocation = ""
   27.85 +        set(value) {
   27.86 +            field = value
   27.87 +            request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
   27.88 +        }
   27.89 +
   27.90 +    /**
   27.91 +     * The view object.
   27.92 +     *
   27.93 +     * @see Constants#REQ_ATTR_VIEWMODEL
   27.94 +     */
   27.95 +    var view: View? = null
   27.96 +        set(value) {
   27.97 +            field = value
   27.98 +            request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
   27.99 +        }
  27.100 +
  27.101 +    /**
  27.102 +     * The base path of this application.
  27.103 +     */
  27.104 +    val baseHref get() = "${request.scheme}://${request.serverName}:${request.serverPort}${request.contextPath}/"
  27.105 +
  27.106 +    private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
  27.107 +    private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
  27.108 +
  27.109 +    fun param(name: String): String? = request.getParameter(name)
  27.110 +    fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
  27.111 +
  27.112 +    fun render(page: String? = null) {
  27.113 +        page?.let { contentPage = it }
  27.114 +        request.getRequestDispatcher(jspPath("site")).forward(request, response)
  27.115 +    }
  27.116 +
  27.117 +    fun renderCommit(location: String? = null) {
  27.118 +        location?.let { redirectLocation = it }
  27.119 +        contentPage = Constants.JSP_COMMIT_SUCCESSFUL
  27.120 +        render()
  27.121 +    }
  27.122 +}
  27.123  
  27.124  /**
  27.125   * A path pattern optionally containing placeholders.
  27.126   *
  27.127   * The special directories . and .. are disallowed in the pattern.
  27.128 - * Placeholders start with a $ sign.
  27.129 + * Placeholders start with a % sign.
  27.130   *
  27.131   * @param pattern the pattern
  27.132   */
  27.133 @@ -91,7 +152,7 @@
  27.134          for (i in nodePatterns.indices) {
  27.135              val pattern = nodePatterns[i]
  27.136              val node = nodes[i]
  27.137 -            if (pattern.startsWith("$")) continue
  27.138 +            if (pattern.startsWith("%")) continue
  27.139              if (pattern != node) return false
  27.140          }
  27.141          return true
  27.142 @@ -106,12 +167,12 @@
  27.143       * @see .matches
  27.144       */
  27.145      fun obtainPathParameters(path: String): PathParameters {
  27.146 -        val params = PathParameters()
  27.147 +        val params = mutableMapOf<String, String>()
  27.148          val nodes = parse(path)
  27.149          for (i in 0 until min(nodes.size, nodePatterns.size)) {
  27.150              val pattern = nodePatterns[i]
  27.151              val node = nodes[i]
  27.152 -            if (pattern.startsWith("$")) {
  27.153 +            if (pattern.startsWith("%")) {
  27.154                  params[pattern.substring(1)] = node
  27.155              }
  27.156          }
  27.157 @@ -121,8 +182,8 @@
  27.158      override fun hashCode(): Int {
  27.159          val str = StringBuilder()
  27.160          for (node in nodePatterns) {
  27.161 -            if (node.startsWith("$")) {
  27.162 -                str.append("/$")
  27.163 +            if (node.startsWith("%")) {
  27.164 +                str.append("/%")
  27.165              } else {
  27.166                  str.append('/')
  27.167                  str.append(node)
  27.168 @@ -138,7 +199,7 @@
  27.169              for (i in nodePatterns.indices) {
  27.170                  val left = nodePatterns[i]
  27.171                  val right = other.nodePatterns[i]
  27.172 -                if (left.startsWith("$") && right.startsWith("$")) continue
  27.173 +                if (left.startsWith("%") && right.startsWith("%")) continue
  27.174                  if (left != right) return false
  27.175              }
  27.176              return true
    28.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    28.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DaoFactory.kt	Fri Apr 02 11:59:14 2021 +0200
    28.3 @@ -0,0 +1,37 @@
    28.4 +/*
    28.5 + * Copyright 2021 Mike Becker. All rights reserved.
    28.6 + *
    28.7 + * Redistribution and use in source and binary forms, with or without
    28.8 + * modification, are permitted provided that the following conditions are met:
    28.9 + *
   28.10 + * 1. Redistributions of source code must retain the above copyright
   28.11 + * notice, this list of conditions and the following disclaimer.
   28.12 + *
   28.13 + * 2. Redistributions in binary form must reproduce the above copyright
   28.14 + * notice, this list of conditions and the following disclaimer in the
   28.15 + * documentation and/or other materials provided with the distribution.
   28.16 + *
   28.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   28.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   28.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   28.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   28.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   28.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   28.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   28.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   28.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   28.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   28.27 + */
   28.28 +
   28.29 +package de.uapcore.lightpit.dao
   28.30 +
   28.31 +import de.uapcore.lightpit.DataSourceProvider
   28.32 +import java.sql.Connection
   28.33 +
   28.34 +fun createDataAccessObject(
   28.35 +    dialect: DataSourceProvider.Dialect,
   28.36 +    connection: Connection
   28.37 +): DataAccessObject =
   28.38 +    when (dialect) {
   28.39 +        DataSourceProvider.Dialect.Postgres -> PostgresDataAccessObject(connection)
   28.40 +    }
   28.41 \ No newline at end of file
    29.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Jan 23 14:47:59 2021 +0100
    29.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Fri Apr 02 11:59:14 2021 +0200
    29.3 @@ -26,7 +26,10 @@
    29.4  package de.uapcore.lightpit.dao
    29.5  
    29.6  import de.uapcore.lightpit.entities.*
    29.7 -import de.uapcore.lightpit.filter.IssueFilter
    29.8 +import de.uapcore.lightpit.util.IssueFilter
    29.9 +import de.uapcore.lightpit.viewmodel.ComponentSummary
   29.10 +import de.uapcore.lightpit.viewmodel.IssueSummary
   29.11 +import de.uapcore.lightpit.viewmodel.VersionSummary
   29.12  
   29.13  interface DataAccessObject {
   29.14      fun listUsers(): List<User>
   29.15 @@ -35,18 +38,29 @@
   29.16      fun insertUser(user: User)
   29.17      fun updateUser(user: User)
   29.18  
   29.19 +    /**
   29.20 +     * Lists all versions of the specified [project].
   29.21 +     *
   29.22 +     * The list is first ordered by the ordinal of the version and
   29.23 +     * then by name, both descending.
   29.24 +     */
   29.25      fun listVersions(project: Project): List<Version>
   29.26 +    fun listVersionSummaries(project: Project): List<VersionSummary>
   29.27      fun findVersion(id: Int): Version?
   29.28      fun findVersionByNode(project: Project, node: String): Version?
   29.29      fun insertVersion(version: Version)
   29.30      fun updateVersion(version: Version)
   29.31  
   29.32      fun listComponents(project: Project): List<Component>
   29.33 +    fun listComponentSummaries(project: Project): List<ComponentSummary>
   29.34      fun findComponent(id: Int): Component?
   29.35      fun findComponentByNode(project: Project, node: String): Component?
   29.36      fun insertComponent(component: Component)
   29.37      fun updateComponent(component: Component)
   29.38  
   29.39 +    /**
   29.40 +     * Lists all projects ordered by name.
   29.41 +     */
   29.42      fun listProjects(): List<Project>
   29.43      fun findProject(id: Int): Project?
   29.44      fun findProjectByNode(node: String): Project?
   29.45 @@ -57,7 +71,7 @@
   29.46  
   29.47      fun listIssues(filter: IssueFilter): List<Issue>
   29.48      fun findIssue(id: Int): Issue?
   29.49 -    fun insertIssue(issue: Issue)
   29.50 +    fun insertIssue(issue: Issue): Int
   29.51      fun updateIssue(issue: Issue)
   29.52  
   29.53      fun listComments(issue: Issue): List<IssueComment>
    30.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Jan 23 14:47:59 2021 +0100
    30.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Fri Apr 02 11:59:14 2021 +0200
    30.3 @@ -26,8 +26,11 @@
    30.4  package de.uapcore.lightpit.dao
    30.5  
    30.6  import de.uapcore.lightpit.entities.*
    30.7 -import de.uapcore.lightpit.filter.*
    30.8  import de.uapcore.lightpit.types.WebColor
    30.9 +import de.uapcore.lightpit.util.*
   30.10 +import de.uapcore.lightpit.viewmodel.ComponentSummary
   30.11 +import de.uapcore.lightpit.viewmodel.IssueSummary
   30.12 +import de.uapcore.lightpit.viewmodel.VersionSummary
   30.13  import java.sql.Connection
   30.14  import java.sql.PreparedStatement
   30.15  import java.sql.ResultSet
   30.16 @@ -129,15 +132,19 @@
   30.17      //</editor-fold>
   30.18  
   30.19      //<editor-fold desc="Version">
   30.20 +
   30.21 +    private fun obtainVersion(rs: ResultSet) =
   30.22 +        Version(rs.getInt("versionid"), rs.getInt("project")).apply {
   30.23 +            name = rs.getString("name")
   30.24 +            node = rs.getString("node")
   30.25 +            ordinal = rs.getInt("ordinal")
   30.26 +            status = rs.getEnum("status")
   30.27 +        }
   30.28 +
   30.29      private fun selectVersions(stmt: PreparedStatement) = sequence {
   30.30          stmt.executeQuery().use { rs ->
   30.31              while (rs.next()) {
   30.32 -                yield(Version(rs.getInt("versionid"), rs.getInt("project")).apply {
   30.33 -                    name = rs.getString("name")
   30.34 -                    node = rs.getString("node")
   30.35 -                    ordinal = rs.getInt("ordinal")
   30.36 -                    status = rs.getEnum("status")
   30.37 -                })
   30.38 +                yield(obtainVersion(rs))
   30.39              }
   30.40          }
   30.41      }
   30.42 @@ -163,6 +170,36 @@
   30.43              """
   30.44          )
   30.45      }
   30.46 +    private val stmtVersionSummaries by lazy {
   30.47 +        connection.prepareStatement(
   30.48 +            """
   30.49 +            with version_map(issueid, versionid, isresolved) as (
   30.50 +                select issueid, versionid, 1
   30.51 +                from lpit_issue_resolved_version
   30.52 +                union
   30.53 +                select issueid, versionid, 0
   30.54 +                from lpit_issue_affected_version
   30.55 +            ),
   30.56 +            issues as (
   30.57 +                select versionid, phase, isresolved, count(issueid) as total
   30.58 +                from lpit_issue
   30.59 +                join version_map using (issueid)
   30.60 +                join lpit_issue_phases using (status)
   30.61 +                group by versionid, phase, isresolved
   30.62 +            ),
   30.63 +            summary as (
   30.64 +                select versionid, phase, isresolved, total
   30.65 +                from lpit_version v
   30.66 +                left join issues using (versionid)
   30.67 +                where v.project = ?
   30.68 +            )
   30.69 +            select versionid, project, name, node, ordinal, status, phase, isresolved, total
   30.70 +            from lpit_version
   30.71 +            join summary using (versionid)
   30.72 +            order by ordinal, name
   30.73 +            """
   30.74 +        )
   30.75 +    }
   30.76      private val stmtVersionByID by lazy {
   30.77          connection.prepareStatement(
   30.78              """${versionQuery}
   30.79 @@ -199,6 +236,27 @@
   30.80          return selectVersions(stmtVersions).toList()
   30.81      }
   30.82  
   30.83 +    override fun listVersionSummaries(project: Project): List<VersionSummary> {
   30.84 +        stmtVersionSummaries.setInt(1, project.id)
   30.85 +        return sequence {
   30.86 +            stmtVersionSummaries.executeQuery().use { rs ->
   30.87 +                while (rs.next()) {
   30.88 +                    val versionSummary = VersionSummary(obtainVersion(rs))
   30.89 +                    val phase = rs.getInt("phase")
   30.90 +                    val total = rs.getInt("total")
   30.91 +                    val issueSummary =
   30.92 +                        if (rs.getBoolean("isresolved")) versionSummary.resolvedTotal else versionSummary.reportedTotal
   30.93 +                    when (phase) {
   30.94 +                        0 -> issueSummary.open = total
   30.95 +                        1 -> issueSummary.active = total
   30.96 +                        2 -> issueSummary.done = total
   30.97 +                    }
   30.98 +                    yield(versionSummary)
   30.99 +                }
  30.100 +            }
  30.101 +        }.toList()
  30.102 +    }
  30.103 +
  30.104      override fun findVersion(id: Int): Version? {
  30.105          stmtVersionByID.setInt(1, id)
  30.106          return selectVersions(stmtVersionByID).firstOrNull()
  30.107 @@ -224,21 +282,25 @@
  30.108      //</editor-fold>
  30.109  
  30.110      //<editor-fold desc="Component">
  30.111 +
  30.112 +    private fun obtainComponent(rs: ResultSet): Component =
  30.113 +        Component(rs.getInt("id"), rs.getInt("project")).apply {
  30.114 +            name = rs.getString("name")
  30.115 +            node = rs.getString("node")
  30.116 +            color = try {
  30.117 +                WebColor(rs.getString("color"))
  30.118 +            } catch (ex: IllegalArgumentException) {
  30.119 +                WebColor("000000")
  30.120 +            }
  30.121 +            ordinal = rs.getInt("ordinal")
  30.122 +            description = rs.getString("description")
  30.123 +            lead = selectUserInfo(rs)
  30.124 +        }
  30.125 +
  30.126      private fun selectComponents(stmt: PreparedStatement) = sequence {
  30.127          stmt.executeQuery().use { rs ->
  30.128              while (rs.next()) {
  30.129 -                yield(Component(rs.getInt("id"), rs.getInt("project")).apply {
  30.130 -                    name = rs.getString("name")
  30.131 -                    node = rs.getString("node")
  30.132 -                    color = try {
  30.133 -                        WebColor(rs.getString("color"))
  30.134 -                    } catch (ex: IllegalArgumentException) {
  30.135 -                        WebColor("000000")
  30.136 -                    }
  30.137 -                    ordinal = rs.getInt("ordinal")
  30.138 -                    description = rs.getString("description")
  30.139 -                    lead = selectUserInfo(rs)
  30.140 -                })
  30.141 +                yield(obtainComponent(rs))
  30.142              }
  30.143          }
  30.144      }
  30.145 @@ -272,6 +334,30 @@
  30.146              """
  30.147          )
  30.148      }
  30.149 +    private val stmtComponentSummaries by lazy {
  30.150 +        connection.prepareStatement(
  30.151 +            """
  30.152 +            with issues as (
  30.153 +                select component, phase, count(issueid) as total
  30.154 +                from lpit_issue
  30.155 +                join lpit_issue_phases using (status)
  30.156 +                group by component, phase
  30.157 +            ),
  30.158 +            summary as (
  30.159 +                select c.id, phase, total
  30.160 +                from lpit_component c
  30.161 +                left join issues i on c.id = i.component 
  30.162 +                where c.project = ?
  30.163 +            )
  30.164 +            select c.id, project, name, node, color, ordinal, description,
  30.165 +                userid, username, givenname, lastname, mail, phase, total
  30.166 +            from lpit_component c
  30.167 +            left join lpit_user on lead = userid
  30.168 +            join summary s on c.id = s.id
  30.169 +            order by ordinal, name
  30.170 +            """
  30.171 +        )
  30.172 +    }
  30.173      private val stmtComponentById by lazy {
  30.174          connection.prepareStatement(
  30.175              """${componentQuery}
  30.176 @@ -305,6 +391,25 @@
  30.177          return selectComponents(stmtComponents).toList()
  30.178      }
  30.179  
  30.180 +    override fun listComponentSummaries(project: Project): List<ComponentSummary> {
  30.181 +        stmtComponentSummaries.setInt(1, project.id)
  30.182 +        return sequence {
  30.183 +            stmtComponentSummaries.executeQuery().use { rs ->
  30.184 +                while (rs.next()) {
  30.185 +                    val componentSummary = ComponentSummary(obtainComponent(rs))
  30.186 +                    val phase = rs.getInt("phase")
  30.187 +                    val total = rs.getInt("total")
  30.188 +                    when (phase) {
  30.189 +                        0 -> componentSummary.issueSummary.open = total
  30.190 +                        1 -> componentSummary.issueSummary.active = total
  30.191 +                        2 -> componentSummary.issueSummary.done = total
  30.192 +                    }
  30.193 +                    yield(componentSummary)
  30.194 +                }
  30.195 +            }
  30.196 +        }.toList()
  30.197 +    }
  30.198 +
  30.199      override fun findComponent(id: Int): Component? {
  30.200          stmtComponentById.setInt(1, id)
  30.201          return selectComponents(stmtComponentById).firstOrNull()
  30.202 @@ -471,7 +576,7 @@
  30.203                              node = rs.getString("componentnode")
  30.204                          }
  30.205                  }
  30.206 -                val issue = Issue(rs.getInt("issueid"), proj, comp).apply {
  30.207 +                val issue = Issue(rs.getInt("issueid"), proj).apply {
  30.208                      component = comp
  30.209                      status = rs.getEnum("status")
  30.210                      category = rs.getEnum("category")
  30.211 @@ -672,14 +777,15 @@
  30.212          }
  30.213      }
  30.214  
  30.215 -    override fun insertIssue(issue: Issue) {
  30.216 +    override fun insertIssue(issue: Issue): Int {
  30.217          val col = setIssueFields(stmtInsertIssue, issue)
  30.218          stmtInsertIssue.setInt(col, issue.project.id)
  30.219 -        stmtInsertIssue.executeQuery().use { rs ->
  30.220 +        val id = stmtInsertIssue.executeQuery().use { rs ->
  30.221              rs.next()
  30.222 -            issue.id = rs.getInt(1)
  30.223 +            rs.getInt(1)
  30.224          }
  30.225          insertVersionInfo(issue)
  30.226 +        return id
  30.227      }
  30.228  
  30.229      override fun updateIssue(issue: Issue) {
    31.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Component.kt	Sat Jan 23 14:47:59 2021 +0100
    31.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Component.kt	Fri Apr 02 11:59:14 2021 +0200
    31.3 @@ -27,11 +27,11 @@
    31.4  
    31.5  import de.uapcore.lightpit.types.WebColor
    31.6  
    31.7 -data class Component(override val id: Int, var projectid: Int) : Entity {
    31.8 +data class Component(override val id: Int, val projectid: Int) : Entity, HasNode {
    31.9      var name: String = ""
   31.10 -    var node: String = name
   31.11 +    override var node: String = name
   31.12 +    var ordinal = 0
   31.13      var color = WebColor("000000")
   31.14 -    var ordinal = 0
   31.15      var description: String? = null
   31.16      var lead: User? = null
   31.17  }
   31.18 \ No newline at end of file
    32.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt	Sat Jan 23 14:47:59 2021 +0100
    32.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt	Fri Apr 02 11:59:14 2021 +0200
    32.3 @@ -27,4 +27,8 @@
    32.4  
    32.5  interface Entity {
    32.6      val id: Int
    32.7 -}
    32.8 \ No newline at end of file
    32.9 +}
   32.10 +
   32.11 +interface HasNode {
   32.12 +    val node: String
   32.13 +}
    33.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt	Sat Jan 23 14:47:59 2021 +0100
    33.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt	Fri Apr 02 11:59:14 2021 +0200
    33.3 @@ -32,13 +32,13 @@
    33.4  import java.sql.Timestamp
    33.5  import java.time.Instant
    33.6  
    33.7 -data class Issue(override var id: Int, var project: Project, var component: Component? = null) : Entity {
    33.8 -
    33.9 +data class Issue(override val id: Int, var project: Project) : Entity {
   33.10 +    var component: Component? = null
   33.11      var status = IssueStatus.InSpecification
   33.12      var category = IssueCategory.Feature
   33.13  
   33.14      var subject: String = ""
   33.15 -    var description: String? = null
   33.16 +    var description: String = ""
   33.17      var assignee: User? = null
   33.18  
   33.19      var created: Timestamp = Timestamp.from(Instant.now())
    34.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/IssueSummary.kt	Sat Jan 23 14:47:59 2021 +0100
    34.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    34.3 @@ -1,53 +0,0 @@
    34.4 -/*
    34.5 - * Copyright 2021 Mike Becker. All rights reserved.
    34.6 - *
    34.7 - * Redistribution and use in source and binary forms, with or without
    34.8 - * modification, are permitted provided that the following conditions are met:
    34.9 - *
   34.10 - * 1. Redistributions of source code must retain the above copyright
   34.11 - * notice, this list of conditions and the following disclaimer.
   34.12 - *
   34.13 - * 2. Redistributions in binary form must reproduce the above copyright
   34.14 - * notice, this list of conditions and the following disclaimer in the
   34.15 - * documentation and/or other materials provided with the distribution.
   34.16 - *
   34.17 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   34.18 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   34.19 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   34.20 - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   34.21 - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   34.22 - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   34.23 - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   34.24 - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   34.25 - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   34.26 - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   34.27 - */
   34.28 -
   34.29 -package de.uapcore.lightpit.entities
   34.30 -
   34.31 -import de.uapcore.lightpit.types.IssueStatusPhase
   34.32 -import kotlin.math.roundToInt
   34.33 -
   34.34 -class IssueSummary {
   34.35 -    var open = 0
   34.36 -    var active = 0
   34.37 -    var done = 0
   34.38 -
   34.39 -    val total get() = open + active + done
   34.40 -
   34.41 -    val openPercent get() = 100 - activePercent - donePercent
   34.42 -    val activePercent get() = if (total > 0) (100f * active / total).roundToInt() else 0
   34.43 -    val donePercent get() = if (total > 0) (100f * done / total).roundToInt() else 100
   34.44 -
   34.45 -    /**
   34.46 -     * Adds the specified issue to the summary by incrementing the respective counter.
   34.47 -     * @param issue the issue
   34.48 -     */
   34.49 -    fun add(issue: Issue) {
   34.50 -        when (issue.status.phase) {
   34.51 -            IssueStatusPhase.Open -> open++
   34.52 -            IssueStatusPhase.WorkInProgress -> active++
   34.53 -            IssueStatusPhase.Done -> done++
   34.54 -        }
   34.55 -    }
   34.56 -}
   34.57 \ No newline at end of file
    35.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Sat Jan 23 14:47:59 2021 +0100
    35.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Fri Apr 02 11:59:14 2021 +0200
    35.3 @@ -25,9 +25,9 @@
    35.4  
    35.5  package de.uapcore.lightpit.entities
    35.6  
    35.7 -data class Project(override val id: Int) : Entity {
    35.8 +data class Project(override val id: Int) : Entity, HasNode {
    35.9      var name: String = ""
   35.10 -    var node: String = name
   35.11 +    override var node: String = name
   35.12      var ordinal = 0
   35.13      var description: String? = null
   35.14      var repoUrl: String? = null
    36.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Version.kt	Sat Jan 23 14:47:59 2021 +0100
    36.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Version.kt	Fri Apr 02 11:59:14 2021 +0200
    36.3 @@ -27,9 +27,9 @@
    36.4  
    36.5  import de.uapcore.lightpit.types.VersionStatus
    36.6  
    36.7 -data class Version(override val id: Int, var projectid: Int) : Entity, Comparable<Version> {
    36.8 +data class Version(override val id: Int, val projectid: Int) : Entity, HasNode, Comparable<Version> {
    36.9      var name: String = ""
   36.10 -    var node = name
   36.11 +    override var node = name
   36.12      var ordinal = 0
   36.13      var status = VersionStatus.Future
   36.14  
    37.1 --- a/src/main/kotlin/de/uapcore/lightpit/filter/Filter.kt	Sat Jan 23 14:47:59 2021 +0100
    37.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    37.3 @@ -1,32 +0,0 @@
    37.4 -/*
    37.5 - * Copyright 2021 Mike Becker. All rights reserved.
    37.6 - *
    37.7 - * Redistribution and use in source and binary forms, with or without
    37.8 - * modification, are permitted provided that the following conditions are met:
    37.9 - *
   37.10 - * 1. Redistributions of source code must retain the above copyright
   37.11 - * notice, this list of conditions and the following disclaimer.
   37.12 - *
   37.13 - * 2. Redistributions in binary form must reproduce the above copyright
   37.14 - * notice, this list of conditions and the following disclaimer in the
   37.15 - * documentation and/or other materials provided with the distribution.
   37.16 - *
   37.17 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   37.18 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   37.19 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   37.20 - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   37.21 - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   37.22 - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   37.23 - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   37.24 - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   37.25 - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   37.26 - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   37.27 - */
   37.28 -
   37.29 -package de.uapcore.lightpit.filter
   37.30 -
   37.31 -sealed class Filter<T>
   37.32 -class AllFilter<T> : Filter<T>()
   37.33 -class NoneFilter<T> : Filter<T>()
   37.34 -data class SpecificFilter<T>(val obj: T) : Filter<T>()
   37.35 -data class RangeFilter<T>(val lower: T, val upper: T) : Filter<T>() where T : Comparable<T>
    38.1 --- a/src/main/kotlin/de/uapcore/lightpit/filter/IssueFilter.kt	Sat Jan 23 14:47:59 2021 +0100
    38.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    38.3 @@ -1,36 +0,0 @@
    38.4 -/*
    38.5 - * Copyright 2021 Mike Becker. All rights reserved.
    38.6 - *
    38.7 - * Redistribution and use in source and binary forms, with or without
    38.8 - * modification, are permitted provided that the following conditions are met:
    38.9 - *
   38.10 - * 1. Redistributions of source code must retain the above copyright
   38.11 - * notice, this list of conditions and the following disclaimer.
   38.12 - *
   38.13 - * 2. Redistributions in binary form must reproduce the above copyright
   38.14 - * notice, this list of conditions and the following disclaimer in the
   38.15 - * documentation and/or other materials provided with the distribution.
   38.16 - *
   38.17 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   38.18 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   38.19 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   38.20 - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   38.21 - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   38.22 - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   38.23 - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   38.24 - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   38.25 - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   38.26 - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   38.27 - */
   38.28 -
   38.29 -package de.uapcore.lightpit.filter
   38.30 -
   38.31 -import de.uapcore.lightpit.entities.Component
   38.32 -import de.uapcore.lightpit.entities.Project
   38.33 -import de.uapcore.lightpit.entities.Version
   38.34 -
   38.35 -data class IssueFilter(
   38.36 -    val project: Filter<Project> = AllFilter(),
   38.37 -    val version: Filter<Version> = AllFilter(),
   38.38 -    val component: Filter<Component> = AllFilter()
   38.39 -)
    39.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    39.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ErrorServlet.kt	Fri Apr 02 11:59:14 2021 +0200
    39.3 @@ -0,0 +1,32 @@
    39.4 +/*
    39.5 + * Copyright 2021 Mike Becker. All rights reserved.
    39.6 + *
    39.7 + * Redistribution and use in source and binary forms, with or without
    39.8 + * modification, are permitted provided that the following conditions are met:
    39.9 + *
   39.10 + * 1. Redistributions of source code must retain the above copyright
   39.11 + * notice, this list of conditions and the following disclaimer.
   39.12 + *
   39.13 + * 2. Redistributions in binary form must reproduce the above copyright
   39.14 + * notice, this list of conditions and the following disclaimer in the
   39.15 + * documentation and/or other materials provided with the distribution.
   39.16 + *
   39.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   39.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   39.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   39.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   39.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   39.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   39.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   39.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   39.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   39.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   39.27 + */
   39.28 +
   39.29 +package de.uapcore.lightpit.servlet
   39.30 +
   39.31 +import de.uapcore.lightpit.AbstractServlet
   39.32 +import javax.servlet.annotation.WebServlet
   39.33 +
   39.34 +@WebServlet(urlPatterns = ["/error/*"])
   39.35 +class ErrorServlet : AbstractServlet()
   39.36 \ No newline at end of file
    40.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    40.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/LanguageServlet.kt	Fri Apr 02 11:59:14 2021 +0200
    40.3 @@ -0,0 +1,69 @@
    40.4 +/*
    40.5 + * Copyright 2021 Mike Becker. All rights reserved.
    40.6 + *
    40.7 + * Redistribution and use in source and binary forms, with or without
    40.8 + * modification, are permitted provided that the following conditions are met:
    40.9 + *
   40.10 + * 1. Redistributions of source code must retain the above copyright
   40.11 + * notice, this list of conditions and the following disclaimer.
   40.12 + *
   40.13 + * 2. Redistributions in binary form must reproduce the above copyright
   40.14 + * notice, this list of conditions and the following disclaimer in the
   40.15 + * documentation and/or other materials provided with the distribution.
   40.16 + *
   40.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   40.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   40.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   40.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   40.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   40.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   40.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   40.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   40.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   40.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   40.27 + */
   40.28 +
   40.29 +package de.uapcore.lightpit.servlet
   40.30 +
   40.31 +import de.uapcore.lightpit.AbstractServlet
   40.32 +import de.uapcore.lightpit.Constants
   40.33 +import de.uapcore.lightpit.HttpRequest
   40.34 +import de.uapcore.lightpit.dao.DataAccessObject
   40.35 +import de.uapcore.lightpit.viewmodel.LanguageView
   40.36 +import java.util.*
   40.37 +import javax.servlet.annotation.WebServlet
   40.38 +
   40.39 +@WebServlet(urlPatterns = ["/language/*"])
   40.40 +class LanguageServlet : AbstractServlet() {
   40.41 +
   40.42 +    init {
   40.43 +        get("/", this::viewLanguages)
   40.44 +        post("/", this::selectLanguage)
   40.45 +    }
   40.46 +
   40.47 +    private fun viewLanguages(http: HttpRequest, dao: DataAccessObject) {
   40.48 +        with(http) {
   40.49 +            view = LanguageView(
   40.50 +                availableLanguages(),
   40.51 +                session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale,
   40.52 +                request.locale
   40.53 +            )
   40.54 +            styleSheets = listOf("language")
   40.55 +            render("language")
   40.56 +        }
   40.57 +    }
   40.58 +
   40.59 +    private fun selectLanguage(http: HttpRequest, dao: DataAccessObject) {
   40.60 +        val lang = http.param("language")
   40.61 +        if (lang != null) {
   40.62 +            val locale = Locale.forLanguageTag(lang)
   40.63 +            if (!locale.language.isNullOrBlank()) {
   40.64 +                http.response.locale = locale
   40.65 +                http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale)
   40.66 +            }
   40.67 +        }
   40.68 +
   40.69 +        viewLanguages(http, dao)
   40.70 +    }
   40.71 +
   40.72 +}
   40.73 \ No newline at end of file
    41.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    41.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Fri Apr 02 11:59:14 2021 +0200
    41.3 @@ -0,0 +1,522 @@
    41.4 +/*
    41.5 + * Copyright 2021 Mike Becker. All rights reserved.
    41.6 + *
    41.7 + * Redistribution and use in source and binary forms, with or without
    41.8 + * modification, are permitted provided that the following conditions are met:
    41.9 + *
   41.10 + * 1. Redistributions of source code must retain the above copyright
   41.11 + * notice, this list of conditions and the following disclaimer.
   41.12 + *
   41.13 + * 2. Redistributions in binary form must reproduce the above copyright
   41.14 + * notice, this list of conditions and the following disclaimer in the
   41.15 + * documentation and/or other materials provided with the distribution.
   41.16 + *
   41.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   41.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   41.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   41.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   41.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   41.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   41.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   41.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   41.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   41.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   41.27 + */
   41.28 +
   41.29 +package de.uapcore.lightpit.servlet
   41.30 +
   41.31 +import de.uapcore.lightpit.AbstractServlet
   41.32 +import de.uapcore.lightpit.HttpRequest
   41.33 +import de.uapcore.lightpit.dao.DataAccessObject
   41.34 +import de.uapcore.lightpit.entities.*
   41.35 +import de.uapcore.lightpit.types.IssueCategory
   41.36 +import de.uapcore.lightpit.types.IssueStatus
   41.37 +import de.uapcore.lightpit.types.VersionStatus
   41.38 +import de.uapcore.lightpit.types.WebColor
   41.39 +import de.uapcore.lightpit.util.AllFilter
   41.40 +import de.uapcore.lightpit.util.IssueFilter
   41.41 +import de.uapcore.lightpit.util.SpecificFilter
   41.42 +import de.uapcore.lightpit.viewmodel.*
   41.43 +import java.sql.Date
   41.44 +import javax.servlet.annotation.WebServlet
   41.45 +
   41.46 +@WebServlet(urlPatterns = ["/projects/*"])
   41.47 +class ProjectServlet : AbstractServlet() {
   41.48 +
   41.49 +    init {
   41.50 +        get("/", this::projects)
   41.51 +        get("/%project", this::project)
   41.52 +        get("/%project/issues/%version/%component/", this::project)
   41.53 +        get("/%project/edit", this::projectForm)
   41.54 +        get("/-/create", this::projectForm)
   41.55 +        post("/-/commit", this::projectCommit)
   41.56 +
   41.57 +        get("/%project/versions/", this::versions)
   41.58 +        get("/%project/versions/%version/edit", this::versionForm)
   41.59 +        get("/%project/versions/-/create", this::versionForm)
   41.60 +        post("/%project/versions/-/commit", this::versionCommit)
   41.61 +
   41.62 +        get("/%project/components/", this::components)
   41.63 +        get("/%project/components/%component/edit", this::componentForm)
   41.64 +        get("/%project/components/-/create", this::componentForm)
   41.65 +        post("/%project/components/-/commit", this::componentCommit)
   41.66 +
   41.67 +        get("/%project/issues/%version/%component/%issue", this::issue)
   41.68 +        get("/%project/issues/%version/%component/%issue/edit", this::issueForm)
   41.69 +        get("/%project/issues/%version/%component/%issue/comment", this::issueComment)
   41.70 +        get("/%project/issues/%version/%component/-/create", this::issueForm)
   41.71 +        get("/%project/issues/%version/%component/-/commit", this::issueCommit)
   41.72 +    }
   41.73 +
   41.74 +    fun projects(http: HttpRequest, dao: DataAccessObject) {
   41.75 +        val projects = dao.listProjects()
   41.76 +        val projectInfos = projects.map {
   41.77 +            ProjectInfo(
   41.78 +                project = it,
   41.79 +                versions = dao.listVersions(it),
   41.80 +                components = emptyList(), // not required in this view
   41.81 +                issueSummary = dao.collectIssueSummary(it)
   41.82 +            )
   41.83 +        }
   41.84 +
   41.85 +        with(http) {
   41.86 +            view = ProjectsView(projectInfos)
   41.87 +            navigationMenu = projectNavMenu(projects)
   41.88 +            styleSheets = listOf("projects")
   41.89 +            render("projects")
   41.90 +        }
   41.91 +    }
   41.92 +
   41.93 +    private fun activeProjectNavMenu(
   41.94 +        projects: List<Project>,
   41.95 +        projectInfo: ProjectInfo,
   41.96 +        selectedVersion: Version? = null,
   41.97 +        selectedComponent: Component? = null
   41.98 +    ) =
   41.99 +        projectNavMenu(
  41.100 +            projects,
  41.101 +            projectInfo.versions,
  41.102 +            projectInfo.components,
  41.103 +            projectInfo.project,
  41.104 +            selectedVersion,
  41.105 +            selectedComponent
  41.106 +        )
  41.107 +
  41.108 +    sealed class LookupResult<T> {
  41.109 +        class NotFound<T> : LookupResult<T>()
  41.110 +        data class Found<T>(val elem: T?) : LookupResult<T>()
  41.111 +    }
  41.112 +
  41.113 +    private fun <T : HasNode> HttpRequest.lookupPathParam(paramName: String, list: List<T>): LookupResult<T> {
  41.114 +        val node = pathParams[paramName]
  41.115 +        return if (node == null || node == "-") {
  41.116 +            LookupResult.Found(null)
  41.117 +        } else {
  41.118 +            val result = list.find { it.node == node }
  41.119 +            if (result == null) {
  41.120 +                LookupResult.NotFound()
  41.121 +            } else {
  41.122 +                LookupResult.Found(result)
  41.123 +            }
  41.124 +        }
  41.125 +    }
  41.126 +
  41.127 +    private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
  41.128 +        val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null
  41.129 +
  41.130 +        val versions: List<Version> = dao.listVersions(project)
  41.131 +        val components: List<Component> = dao.listComponents(project)
  41.132 +
  41.133 +        return ProjectInfo(
  41.134 +            project,
  41.135 +            versions,
  41.136 +            components,
  41.137 +            dao.collectIssueSummary(project)
  41.138 +        )
  41.139 +    }
  41.140 +
  41.141 +    private fun sanitizeNode(name: String): String {
  41.142 +        val san = name.replace(Regex("[/\\\\]"), "-")
  41.143 +        return if (san.startsWith(".")) {
  41.144 +            "v$san"
  41.145 +        } else {
  41.146 +            san
  41.147 +        }
  41.148 +    }
  41.149 +
  41.150 +    data class PathInfos(
  41.151 +        val projectInfo: ProjectInfo,
  41.152 +        val version: Version?,
  41.153 +        val component: Component?
  41.154 +    ) {
  41.155 +        val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/")
  41.156 +    }
  41.157 +
  41.158 +    private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? {
  41.159 +        val projectInfo = obtainProjectInfo(http, dao)
  41.160 +        if (projectInfo == null) {
  41.161 +            http.response.sendError(404)
  41.162 +            return null
  41.163 +        }
  41.164 +
  41.165 +        val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) {
  41.166 +            is LookupResult.NotFound -> {
  41.167 +                http.response.sendError(404)
  41.168 +                return null
  41.169 +            }
  41.170 +            is LookupResult.Found -> {
  41.171 +                result.elem
  41.172 +            }
  41.173 +        }
  41.174 +        val component = when (val result = http.lookupPathParam("component", projectInfo.components)) {
  41.175 +            is LookupResult.NotFound -> {
  41.176 +                http.response.sendError(404)
  41.177 +                return null
  41.178 +            }
  41.179 +            is LookupResult.Found -> {
  41.180 +                result.elem
  41.181 +            }
  41.182 +        }
  41.183 +
  41.184 +        return PathInfos(projectInfo, version, component)
  41.185 +    }
  41.186 +
  41.187 +    fun project(http: HttpRequest, dao: DataAccessObject) {
  41.188 +        withPathInfo(http, dao)?.run {
  41.189 +            val issues = dao.listIssues(IssueFilter(
  41.190 +                project = SpecificFilter(projectInfo.project),
  41.191 +                version = version?.let { SpecificFilter(it) } ?: AllFilter(),
  41.192 +                component = component?.let { SpecificFilter(it) } ?: AllFilter()
  41.193 +            ))
  41.194 +
  41.195 +            with(http) {
  41.196 +                view = ProjectDetails(projectInfo, issues, version, component)
  41.197 +                navigationMenu = activeProjectNavMenu(
  41.198 +                    dao.listProjects(),
  41.199 +                    projectInfo,
  41.200 +                    version,
  41.201 +                    component
  41.202 +                )
  41.203 +                styleSheets = listOf("projects")
  41.204 +                render("project-details")
  41.205 +            }
  41.206 +        }
  41.207 +    }
  41.208 +
  41.209 +    fun projectForm(http: HttpRequest, dao: DataAccessObject) {
  41.210 +        val projectInfo = obtainProjectInfo(http, dao)
  41.211 +        if (projectInfo == null) {
  41.212 +            http.response.sendError(404)
  41.213 +            return
  41.214 +        }
  41.215 +
  41.216 +        with(http) {
  41.217 +            view = ProjectEditView(projectInfo.project, dao.listUsers())
  41.218 +            navigationMenu = activeProjectNavMenu(
  41.219 +                dao.listProjects(),
  41.220 +                projectInfo
  41.221 +            )
  41.222 +            styleSheets = listOf("projects")
  41.223 +            render("project-form")
  41.224 +        }
  41.225 +    }
  41.226 +
  41.227 +    fun projectCommit(http: HttpRequest, dao: DataAccessObject) {
  41.228 +        // TODO: replace defaults with throwing validator exceptions
  41.229 +        val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply {
  41.230 +            name = http.param("name") ?: ""
  41.231 +            node = http.param("node") ?: ""
  41.232 +            description = http.param("description") ?: ""
  41.233 +            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
  41.234 +            repoUrl = http.param("repoUrl") ?: ""
  41.235 +            owner = (http.param("owner")?.toIntOrNull() ?: -1).let {
  41.236 +                if (it < 0) null else dao.findUser(it)
  41.237 +            }
  41.238 +            // intentional defaults
  41.239 +            if (node.isBlank()) node = name
  41.240 +            // sanitizing
  41.241 +            node = sanitizeNode(node)
  41.242 +        }
  41.243 +
  41.244 +        if (project.id < 0) {
  41.245 +            dao.insertProject(project)
  41.246 +        } else {
  41.247 +            dao.updateProject(project)
  41.248 +        }
  41.249 +
  41.250 +        http.renderCommit("projects/${project.node}")
  41.251 +    }
  41.252 +
  41.253 +    fun versions(http: HttpRequest, dao: DataAccessObject) {
  41.254 +        val projectInfo = obtainProjectInfo(http, dao)
  41.255 +        if (projectInfo == null) {
  41.256 +            http.response.sendError(404)
  41.257 +            return
  41.258 +        }
  41.259 +
  41.260 +        with(http) {
  41.261 +            view = VersionsView(
  41.262 +                projectInfo,
  41.263 +                dao.listVersionSummaries(projectInfo.project)
  41.264 +            )
  41.265 +            navigationMenu = activeProjectNavMenu(
  41.266 +                dao.listProjects(),
  41.267 +                projectInfo
  41.268 +            )
  41.269 +            styleSheets = listOf("projects")
  41.270 +            render("versions")
  41.271 +        }
  41.272 +    }
  41.273 +
  41.274 +    fun versionForm(http: HttpRequest, dao: DataAccessObject) {
  41.275 +        val projectInfo = obtainProjectInfo(http, dao)
  41.276 +        if (projectInfo == null) {
  41.277 +            http.response.sendError(404)
  41.278 +            return
  41.279 +        }
  41.280 +
  41.281 +        val version: Version
  41.282 +        when (val result = http.lookupPathParam("version", projectInfo.versions)) {
  41.283 +            is LookupResult.NotFound -> {
  41.284 +                http.response.sendError(404)
  41.285 +                return
  41.286 +            }
  41.287 +            is LookupResult.Found -> {
  41.288 +                version = result.elem ?: Version(-1, projectInfo.project.id)
  41.289 +            }
  41.290 +        }
  41.291 +
  41.292 +        with(http) {
  41.293 +            view = VersionEditView(projectInfo, version)
  41.294 +            navigationMenu = activeProjectNavMenu(
  41.295 +                dao.listProjects(),
  41.296 +                projectInfo,
  41.297 +                selectedVersion = version
  41.298 +            )
  41.299 +            styleSheets = listOf("projects")
  41.300 +            render("version-form")
  41.301 +        }
  41.302 +    }
  41.303 +
  41.304 +    fun versionCommit(http: HttpRequest, dao: DataAccessObject) {
  41.305 +        val id = http.param("id")?.toIntOrNull()
  41.306 +        val projectid = http.param("projectid")?.toIntOrNull() ?: -1
  41.307 +        val project = dao.findProject(projectid)
  41.308 +        if (id == null || project == null) {
  41.309 +            http.response.sendError(400)
  41.310 +            return
  41.311 +        }
  41.312 +
  41.313 +        // TODO: replace defaults with throwing validator exceptions
  41.314 +        val version = Version(id, projectid).apply {
  41.315 +            name = http.param("name") ?: ""
  41.316 +            node = http.param("node") ?: ""
  41.317 +            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
  41.318 +            status = http.param("status")?.let(VersionStatus::valueOf) ?: VersionStatus.Future
  41.319 +            // intentional defaults
  41.320 +            if (node.isBlank()) node = name
  41.321 +            // sanitizing
  41.322 +            node = sanitizeNode(node)
  41.323 +        }
  41.324 +
  41.325 +        if (id < 0) {
  41.326 +            dao.insertVersion(version)
  41.327 +        } else {
  41.328 +            dao.updateVersion(version)
  41.329 +        }
  41.330 +
  41.331 +        http.renderCommit("projects/${project.node}/versions/")
  41.332 +    }
  41.333 +
  41.334 +    fun components(http: HttpRequest, dao: DataAccessObject) {
  41.335 +        val projectInfo = obtainProjectInfo(http, dao)
  41.336 +        if (projectInfo == null) {
  41.337 +            http.response.sendError(404)
  41.338 +            return
  41.339 +        }
  41.340 +
  41.341 +        with(http) {
  41.342 +            view = ComponentsView(
  41.343 +                projectInfo,
  41.344 +                dao.listComponentSummaries(projectInfo.project)
  41.345 +            )
  41.346 +            navigationMenu = activeProjectNavMenu(
  41.347 +                dao.listProjects(),
  41.348 +                projectInfo
  41.349 +            )
  41.350 +            styleSheets = listOf("projects")
  41.351 +            render("components")
  41.352 +        }
  41.353 +    }
  41.354 +
  41.355 +    fun componentForm(http: HttpRequest, dao: DataAccessObject) {
  41.356 +        val projectInfo = obtainProjectInfo(http, dao)
  41.357 +        if (projectInfo == null) {
  41.358 +            http.response.sendError(404)
  41.359 +            return
  41.360 +        }
  41.361 +
  41.362 +        val component: Component
  41.363 +        when (val result = http.lookupPathParam("component", projectInfo.components)) {
  41.364 +            is LookupResult.NotFound -> {
  41.365 +                http.response.sendError(404)
  41.366 +                return
  41.367 +            }
  41.368 +            is LookupResult.Found -> {
  41.369 +                component = result.elem ?: Component(-1, projectInfo.project.id)
  41.370 +            }
  41.371 +        }
  41.372 +
  41.373 +        with(http) {
  41.374 +            view = ComponentEditView(projectInfo, component, dao.listUsers())
  41.375 +            navigationMenu = activeProjectNavMenu(
  41.376 +                dao.listProjects(),
  41.377 +                projectInfo,
  41.378 +                selectedComponent = component
  41.379 +            )
  41.380 +            styleSheets = listOf("projects")
  41.381 +            render("component-form")
  41.382 +        }
  41.383 +    }
  41.384 +
  41.385 +    fun componentCommit(http: HttpRequest, dao: DataAccessObject) {
  41.386 +        val id = http.param("id")?.toIntOrNull()
  41.387 +        val projectid = http.param("projectid")?.toIntOrNull() ?: -1
  41.388 +        val project = dao.findProject(projectid)
  41.389 +        if (id == null || project == null) {
  41.390 +            http.response.sendError(400)
  41.391 +            return
  41.392 +        }
  41.393 +
  41.394 +        // TODO: replace defaults with throwing validator exceptions
  41.395 +        val component = Component(id, projectid).apply {
  41.396 +            name = http.param("name") ?: ""
  41.397 +            node = http.param("node") ?: ""
  41.398 +            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
  41.399 +            color = WebColor(http.param("color") ?: "#000000")
  41.400 +            description = http.param("description")
  41.401 +            lead = (http.param("lead")?.toIntOrNull() ?: -1).let {
  41.402 +                if (it < 0) null else dao.findUser(it)
  41.403 +            }
  41.404 +            // intentional defaults
  41.405 +            if (node.isBlank()) node = name
  41.406 +            // sanitizing
  41.407 +            node = sanitizeNode(node)
  41.408 +        }
  41.409 +
  41.410 +        if (id < 0) {
  41.411 +            dao.insertComponent(component)
  41.412 +        } else {
  41.413 +            dao.updateComponent(component)
  41.414 +        }
  41.415 +
  41.416 +        http.renderCommit("projects/${project.node}/components/")
  41.417 +    }
  41.418 +
  41.419 +    fun issue(http: HttpRequest, dao: DataAccessObject) {
  41.420 +        withPathInfo(http, dao)?.run {
  41.421 +            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1)
  41.422 +            if (issue == null) {
  41.423 +                http.response.sendError(404)
  41.424 +                return
  41.425 +            }
  41.426 +
  41.427 +            val comments = dao.listComments(issue)
  41.428 +
  41.429 +            with(http) {
  41.430 +                view = IssueDetailView(issue, comments, projectInfo.project, version, component)
  41.431 +                navigationMenu = activeProjectNavMenu(
  41.432 +                    dao.listProjects(),
  41.433 +                    projectInfo,
  41.434 +                    version,
  41.435 +                    component
  41.436 +                )
  41.437 +                styleSheets = listOf("projects")
  41.438 +                render("issue-view")
  41.439 +            }
  41.440 +        }
  41.441 +    }
  41.442 +
  41.443 +    fun issueForm(http: HttpRequest, dao: DataAccessObject) {
  41.444 +        withPathInfo(http, dao)?.run {
  41.445 +            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue(
  41.446 +                -1,
  41.447 +                projectInfo.project,
  41.448 +            )
  41.449 +
  41.450 +            // pre-select component, if available in the path info
  41.451 +            issue.component = component
  41.452 +
  41.453 +            with(http) {
  41.454 +                view = IssueEditView(
  41.455 +                    issue,
  41.456 +                    projectInfo.versions,
  41.457 +                    projectInfo.components,
  41.458 +                    dao.listUsers(),
  41.459 +                    projectInfo.project,
  41.460 +                    version,
  41.461 +                    component
  41.462 +                )
  41.463 +                navigationMenu = activeProjectNavMenu(
  41.464 +                    dao.listProjects(),
  41.465 +                    projectInfo,
  41.466 +                    version,
  41.467 +                    component
  41.468 +                )
  41.469 +                styleSheets = listOf("projects")
  41.470 +                render("issue-form")
  41.471 +            }
  41.472 +        }
  41.473 +    }
  41.474 +
  41.475 +    fun issueComment(http: HttpRequest, dao: DataAccessObject) {
  41.476 +        withPathInfo(http, dao)?.run {
  41.477 +            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1)
  41.478 +            if (issue == null) {
  41.479 +                http.response.sendError(404)
  41.480 +                return
  41.481 +            }
  41.482 +
  41.483 +            // TODO: throw validator exception instead of using a default
  41.484 +            val comment = IssueComment(-1, issue.id).apply {
  41.485 +                author = http.remoteUser?.let { dao.findUserByName(it) }
  41.486 +                comment = http.param("comment") ?: ""
  41.487 +            }
  41.488 +
  41.489 +            dao.insertComment(comment)
  41.490 +
  41.491 +            http.renderCommit("${issuesHref}${issue.id}")
  41.492 +        }
  41.493 +    }
  41.494 +
  41.495 +    fun issueCommit(http: HttpRequest, dao: DataAccessObject) {
  41.496 +        withPathInfo(http, dao)?.run {
  41.497 +            // TODO: throw validator exception instead of using defaults
  41.498 +            val issue = Issue(
  41.499 +                http.param("id")?.toIntOrNull() ?: -1,
  41.500 +                projectInfo.project
  41.501 +            ).apply {
  41.502 +                component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
  41.503 +                category = IssueCategory.valueOf(http.param("category") ?: "")
  41.504 +                status = IssueStatus.valueOf(http.param("status") ?: "")
  41.505 +                subject = http.param("subject") ?: ""
  41.506 +                description = http.param("description") ?: ""
  41.507 +                assignee = http.param("assignee")?.toIntOrNull()?.let {
  41.508 +                    when (it) {
  41.509 +                        -1 -> null
  41.510 +                        -2 -> component?.lead
  41.511 +                        else -> dao.findUser(it)
  41.512 +                    }
  41.513 +                }
  41.514 +                eta = http.param("eta")?.let { Date.valueOf(it) }
  41.515 +
  41.516 +                affectedVersions = http.paramArray("affected")
  41.517 +                    .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } }
  41.518 +                resolvedVersions = http.paramArray("resolved")
  41.519 +                    .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } }
  41.520 +            }
  41.521 +
  41.522 +            http.renderCommit("${issuesHref}${issue.id}")
  41.523 +        }
  41.524 +    }
  41.525 +}
  41.526 \ No newline at end of file
    42.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    42.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt	Fri Apr 02 11:59:14 2021 +0200
    42.3 @@ -0,0 +1,112 @@
    42.4 +/*
    42.5 + * Copyright 2021 Mike Becker. All rights reserved.
    42.6 + *
    42.7 + * Redistribution and use in source and binary forms, with or without
    42.8 + * modification, are permitted provided that the following conditions are met:
    42.9 + *
   42.10 + * 1. Redistributions of source code must retain the above copyright
   42.11 + * notice, this list of conditions and the following disclaimer.
   42.12 + *
   42.13 + * 2. Redistributions in binary form must reproduce the above copyright
   42.14 + * notice, this list of conditions and the following disclaimer in the
   42.15 + * documentation and/or other materials provided with the distribution.
   42.16 + *
   42.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   42.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   42.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   42.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   42.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   42.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   42.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   42.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   42.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   42.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   42.27 + */
   42.28 +
   42.29 +package de.uapcore.lightpit.servlet
   42.30 +
   42.31 +import de.uapcore.lightpit.AbstractServlet
   42.32 +import de.uapcore.lightpit.HttpRequest
   42.33 +import de.uapcore.lightpit.LoggingTrait
   42.34 +import de.uapcore.lightpit.dao.DataAccessObject
   42.35 +import de.uapcore.lightpit.entities.User
   42.36 +import de.uapcore.lightpit.logger
   42.37 +import de.uapcore.lightpit.viewmodel.UserEditView
   42.38 +import de.uapcore.lightpit.viewmodel.UsersView
   42.39 +import javax.servlet.annotation.WebServlet
   42.40 +
   42.41 +@WebServlet(urlPatterns = ["/users/*"])
   42.42 +class UsersServlet : AbstractServlet(), LoggingTrait {
   42.43 +
   42.44 +    init {
   42.45 +        get("/", this::index)
   42.46 +        get("/-/create", this::create)
   42.47 +        get("/%userid/edit", this::edit)
   42.48 +        post("/-/commit", this::commit)
   42.49 +    }
   42.50 +
   42.51 +    private val list = "users"
   42.52 +    private val form = "user-form"
   42.53 +
   42.54 +    fun index(http: HttpRequest, dao: DataAccessObject) {
   42.55 +        with(http) {
   42.56 +            view = UsersView(dao.listUsers())
   42.57 +            render(list)
   42.58 +        }
   42.59 +    }
   42.60 +
   42.61 +    fun create(http: HttpRequest, dao: DataAccessObject) {
   42.62 +        with(http) {
   42.63 +            view = UserEditView(User(-1))
   42.64 +            render(form)
   42.65 +        }
   42.66 +    }
   42.67 +
   42.68 +    fun edit(http: HttpRequest, dao: DataAccessObject) {
   42.69 +        val id = http.pathParams["userid"]?.toIntOrNull()
   42.70 +        if (id == null) {
   42.71 +            http.response.sendError(404)
   42.72 +        } else {
   42.73 +            val user = dao.findUser(id)
   42.74 +            if (user == null) {
   42.75 +                http.response.sendError(404)
   42.76 +            } else {
   42.77 +                with(http) {
   42.78 +                    view = UserEditView(user)
   42.79 +                    render(form)
   42.80 +                }
   42.81 +            }
   42.82 +        }
   42.83 +    }
   42.84 +
   42.85 +    fun commit(http: HttpRequest, dao: DataAccessObject) {
   42.86 +        val id = http.param("userid")?.toIntOrNull()
   42.87 +        if (id == null) {
   42.88 +            http.response.sendError(400)
   42.89 +            return
   42.90 +        }
   42.91 +
   42.92 +        val user = User(id)
   42.93 +        with(user) {
   42.94 +            username = http.param("username") ?: ""
   42.95 +            givenname = http.param("givenname")
   42.96 +            lastname = http.param("lastname")
   42.97 +            mail = http.param("mail")
   42.98 +        }
   42.99 +
  42.100 +        if (dao.findUserByName(user.username) != null) {
  42.101 +            with(http) {
  42.102 +                view = UserEditView(user).apply { errorText = "validation.username.unique" }
  42.103 +            }
  42.104 +        }
  42.105 +
  42.106 +        if (user.id > 0) {
  42.107 +            logger().info("Update user ${user.username} with id ${user.id}.")
  42.108 +            dao.updateUser(user)
  42.109 +        } else {
  42.110 +            logger().info("Insert user ${user.username}.")
  42.111 +            dao.insertUser(user)
  42.112 +        }
  42.113 +        http.renderCommit("users/")
  42.114 +    }
  42.115 +}
  42.116 \ No newline at end of file
    43.1 --- a/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt	Sat Jan 23 14:47:59 2021 +0100
    43.2 +++ b/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt	Fri Apr 02 11:59:14 2021 +0200
    43.3 @@ -27,7 +27,7 @@
    43.4  
    43.5  
    43.6  /**
    43.7 - * Represents a web color in hexadezimal representation.
    43.8 + * Represents a web color in hexadecimal representation.
    43.9   * @param arg the 6 digits hex string optionally preceded by a hash symbol
   43.10   */
   43.11  class WebColor(arg: String) {
    44.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    44.2 +++ b/src/main/kotlin/de/uapcore/lightpit/util/Filter.kt	Fri Apr 02 11:59:14 2021 +0200
    44.3 @@ -0,0 +1,32 @@
    44.4 +/*
    44.5 + * Copyright 2021 Mike Becker. All rights reserved.
    44.6 + *
    44.7 + * Redistribution and use in source and binary forms, with or without
    44.8 + * modification, are permitted provided that the following conditions are met:
    44.9 + *
   44.10 + * 1. Redistributions of source code must retain the above copyright
   44.11 + * notice, this list of conditions and the following disclaimer.
   44.12 + *
   44.13 + * 2. Redistributions in binary form must reproduce the above copyright
   44.14 + * notice, this list of conditions and the following disclaimer in the
   44.15 + * documentation and/or other materials provided with the distribution.
   44.16 + *
   44.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   44.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   44.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   44.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   44.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   44.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   44.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   44.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   44.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   44.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   44.27 + */
   44.28 +
   44.29 +package de.uapcore.lightpit.util
   44.30 +
   44.31 +sealed class Filter<T>
   44.32 +class AllFilter<T> : Filter<T>()
   44.33 +class NoneFilter<T> : Filter<T>()
   44.34 +data class SpecificFilter<T>(val obj: T) : Filter<T>()
   44.35 +data class RangeFilter<T>(val lower: T, val upper: T) : Filter<T>() where T : Comparable<T>
    45.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    45.2 +++ b/src/main/kotlin/de/uapcore/lightpit/util/Issues.kt	Fri Apr 02 11:59:14 2021 +0200
    45.3 @@ -0,0 +1,71 @@
    45.4 +/*
    45.5 + * Copyright 2021 Mike Becker. All rights reserved.
    45.6 + *
    45.7 + * Redistribution and use in source and binary forms, with or without
    45.8 + * modification, are permitted provided that the following conditions are met:
    45.9 + *
   45.10 + * 1. Redistributions of source code must retain the above copyright
   45.11 + * notice, this list of conditions and the following disclaimer.
   45.12 + *
   45.13 + * 2. Redistributions in binary form must reproduce the above copyright
   45.14 + * notice, this list of conditions and the following disclaimer in the
   45.15 + * documentation and/or other materials provided with the distribution.
   45.16 + *
   45.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   45.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   45.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   45.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   45.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   45.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   45.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   45.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   45.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   45.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   45.27 + */
   45.28 +
   45.29 +package de.uapcore.lightpit.util
   45.30 +
   45.31 +import de.uapcore.lightpit.entities.Component
   45.32 +import de.uapcore.lightpit.entities.Issue
   45.33 +import de.uapcore.lightpit.entities.Project
   45.34 +import de.uapcore.lightpit.entities.Version
   45.35 +import de.uapcore.lightpit.types.IssueStatusPhase
   45.36 +
   45.37 +data class IssueFilter(
   45.38 +    val project: Filter<Project> = AllFilter(),
   45.39 +    val version: Filter<Version> = AllFilter(),
   45.40 +    val component: Filter<Component> = AllFilter()
   45.41 +)
   45.42 +
   45.43 +data class IssueSorter(val criteria: List<Criteria>) : Comparator<Issue> {
   45.44 +    enum class Field {
   45.45 +        DONE, ETA, UPDATED
   45.46 +    }
   45.47 +
   45.48 +    data class Criteria(val field: Field, val asc: Boolean)
   45.49 +
   45.50 +    override fun compare(left: Issue, right: Issue): Int {
   45.51 +        if (left == right) {
   45.52 +            return 0;
   45.53 +        }
   45.54 +        for (c in criteria) {
   45.55 +            val result = when (c.field) {
   45.56 +                Field.DONE -> (left.status.phase == IssueStatusPhase.Done).compareTo(right.status.phase == IssueStatusPhase.Done)
   45.57 +                Field.ETA -> {
   45.58 +                    val l = left.eta
   45.59 +                    val r = right.eta
   45.60 +                    if (l == null && r == null) 0
   45.61 +                    else if (l == null) 1
   45.62 +                    else if (r == null) -1
   45.63 +                    else l.compareTo(r)
   45.64 +                }
   45.65 +                Field.UPDATED -> left.updated.compareTo(right.updated)
   45.66 +            }
   45.67 +            if (result != 0) {
   45.68 +                return if (c.asc) result else -result
   45.69 +            }
   45.70 +        }
   45.71 +        return 0
   45.72 +    }
   45.73 +}
   45.74 +
    46.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    46.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Components.kt	Fri Apr 02 11:59:14 2021 +0200
    46.3 @@ -0,0 +1,46 @@
    46.4 +/*
    46.5 + * Copyright 2021 Mike Becker. All rights reserved.
    46.6 + *
    46.7 + * Redistribution and use in source and binary forms, with or without
    46.8 + * modification, are permitted provided that the following conditions are met:
    46.9 + *
   46.10 + * 1. Redistributions of source code must retain the above copyright
   46.11 + * notice, this list of conditions and the following disclaimer.
   46.12 + *
   46.13 + * 2. Redistributions in binary form must reproduce the above copyright
   46.14 + * notice, this list of conditions and the following disclaimer in the
   46.15 + * documentation and/or other materials provided with the distribution.
   46.16 + *
   46.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   46.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   46.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   46.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   46.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   46.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   46.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   46.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   46.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   46.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   46.27 + */
   46.28 +
   46.29 +package de.uapcore.lightpit.viewmodel
   46.30 +
   46.31 +import de.uapcore.lightpit.entities.Component
   46.32 +import de.uapcore.lightpit.entities.User
   46.33 +
   46.34 +class ComponentSummary(
   46.35 +    val component: Component,
   46.36 +) {
   46.37 +    val issueSummary = IssueSummary()
   46.38 +}
   46.39 +
   46.40 +class ComponentsView(
   46.41 +    val projectInfo: ProjectInfo,
   46.42 +    val componentInfos: List<ComponentSummary>
   46.43 +) : View()
   46.44 +
   46.45 +class ComponentEditView(
   46.46 +    val projectInfo: ProjectInfo,
   46.47 +    val component: Component,
   46.48 +    val users: List<User>
   46.49 +) : EditView()
    47.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    47.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Fri Apr 02 11:59:14 2021 +0200
    47.3 @@ -0,0 +1,118 @@
    47.4 +/*
    47.5 + * Copyright 2021 Mike Becker. All rights reserved.
    47.6 + *
    47.7 + * Redistribution and use in source and binary forms, with or without
    47.8 + * modification, are permitted provided that the following conditions are met:
    47.9 + *
   47.10 + * 1. Redistributions of source code must retain the above copyright
   47.11 + * notice, this list of conditions and the following disclaimer.
   47.12 + *
   47.13 + * 2. Redistributions in binary form must reproduce the above copyright
   47.14 + * notice, this list of conditions and the following disclaimer in the
   47.15 + * documentation and/or other materials provided with the distribution.
   47.16 + *
   47.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   47.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   47.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   47.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   47.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   47.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   47.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   47.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   47.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   47.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   47.27 + */
   47.28 +
   47.29 +package de.uapcore.lightpit.viewmodel
   47.30 +
   47.31 +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
   47.32 +import com.vladsch.flexmark.ext.tables.TablesExtension
   47.33 +import com.vladsch.flexmark.html.HtmlRenderer
   47.34 +import com.vladsch.flexmark.parser.Parser
   47.35 +import com.vladsch.flexmark.util.data.MutableDataSet
   47.36 +import de.uapcore.lightpit.entities.*
   47.37 +import de.uapcore.lightpit.types.IssueCategory
   47.38 +import de.uapcore.lightpit.types.IssueStatus
   47.39 +import de.uapcore.lightpit.types.IssueStatusPhase
   47.40 +import de.uapcore.lightpit.types.VersionStatus
   47.41 +import kotlin.math.roundToInt
   47.42 +
   47.43 +class IssueSummary {
   47.44 +    var open = 0
   47.45 +    var active = 0
   47.46 +    var done = 0
   47.47 +
   47.48 +    val total get() = open + active + done
   47.49 +
   47.50 +    val openPercent get() = 100 - activePercent - donePercent
   47.51 +    val activePercent get() = if (total > 0) (100f * active / total).roundToInt() else 0
   47.52 +    val donePercent get() = if (total > 0) (100f * done / total).roundToInt() else 100
   47.53 +
   47.54 +    /**
   47.55 +     * Adds the specified issue to the summary by incrementing the respective counter.
   47.56 +     * @param issue the issue
   47.57 +     */
   47.58 +    fun add(issue: Issue) {
   47.59 +        when (issue.status.phase) {
   47.60 +            IssueStatusPhase.Open -> open++
   47.61 +            IssueStatusPhase.WorkInProgress -> active++
   47.62 +            IssueStatusPhase.Done -> done++
   47.63 +        }
   47.64 +    }
   47.65 +}
   47.66 +
   47.67 +class IssueDetailView(
   47.68 +    val issue: Issue,
   47.69 +    val comments: List<IssueComment>,
   47.70 +    val project: Project,
   47.71 +    val version: Version? = null,
   47.72 +    val component: Component? = null
   47.73 +) : View() {
   47.74 +
   47.75 +    init {
   47.76 +        val options = MutableDataSet()
   47.77 +            .set(Parser.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create()))
   47.78 +        val parser = Parser.builder(options).build()
   47.79 +        val renderer = HtmlRenderer.builder(options).build()
   47.80 +        val process = fun(it: String) = renderer.render(parser.parse(it))
   47.81 +
   47.82 +        issue.description = process(issue.description)
   47.83 +        for (comment in comments) {
   47.84 +            comment.comment = process(comment.comment)
   47.85 +        }
   47.86 +    }
   47.87 +}
   47.88 +
   47.89 +class IssueEditView(
   47.90 +    val issue: Issue,
   47.91 +    val versions: List<Version>,
   47.92 +    val components: List<Component>,
   47.93 +    val users: List<User>,
   47.94 +    val project: Project, // TODO: allow null values to create issues from the IssuesServlet
   47.95 +    val version: Version? = null,
   47.96 +    val component: Component? = null
   47.97 +) : EditView() {
   47.98 +
   47.99 +    val versionsUpcoming: List<Version>
  47.100 +    val versionsRecent: List<Version>
  47.101 +
  47.102 +    val issueStatus = IssueStatus.values()
  47.103 +    val issueCategory = IssueCategory.values()
  47.104 +
  47.105 +    init {
  47.106 +        val recent = mutableListOf<Version>()
  47.107 +        val upcoming = mutableListOf<Version>()
  47.108 +        recent.addAll(issue.affectedVersions)
  47.109 +        upcoming.addAll(issue.resolvedVersions)
  47.110 +        for (v in versions) {
  47.111 +            if (v.status.isReleased) {
  47.112 +                if (v.status != VersionStatus.Deprecated) recent.add(v)
  47.113 +            } else {
  47.114 +                upcoming.add(v)
  47.115 +            }
  47.116 +        }
  47.117 +        versionsRecent = recent
  47.118 +        versionsUpcoming = upcoming
  47.119 +    }
  47.120 +}
  47.121 +
    48.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    48.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/LanguageView.kt	Fri Apr 02 11:59:14 2021 +0200
    48.3 @@ -0,0 +1,34 @@
    48.4 +/*
    48.5 + * Copyright 2021 Mike Becker. All rights reserved.
    48.6 + *
    48.7 + * Redistribution and use in source and binary forms, with or without
    48.8 + * modification, are permitted provided that the following conditions are met:
    48.9 + *
   48.10 + * 1. Redistributions of source code must retain the above copyright
   48.11 + * notice, this list of conditions and the following disclaimer.
   48.12 + *
   48.13 + * 2. Redistributions in binary form must reproduce the above copyright
   48.14 + * notice, this list of conditions and the following disclaimer in the
   48.15 + * documentation and/or other materials provided with the distribution.
   48.16 + *
   48.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   48.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   48.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   48.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   48.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   48.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   48.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   48.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   48.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   48.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   48.27 + */
   48.28 +
   48.29 +package de.uapcore.lightpit.viewmodel
   48.30 +
   48.31 +import java.util.*
   48.32 +
   48.33 +class LanguageView(
   48.34 +    val languages: List<Locale>,
   48.35 +    val currentLanguage: Locale,
   48.36 +    val browserLanguage: Locale
   48.37 +) : View()
   48.38 \ No newline at end of file
    49.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    49.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt	Fri Apr 02 11:59:14 2021 +0200
    49.3 @@ -0,0 +1,128 @@
    49.4 +/*
    49.5 + * Copyright 2021 Mike Becker. All rights reserved.
    49.6 + *
    49.7 + * Redistribution and use in source and binary forms, with or without
    49.8 + * modification, are permitted provided that the following conditions are met:
    49.9 + *
   49.10 + * 1. Redistributions of source code must retain the above copyright
   49.11 + * notice, this list of conditions and the following disclaimer.
   49.12 + *
   49.13 + * 2. Redistributions in binary form must reproduce the above copyright
   49.14 + * notice, this list of conditions and the following disclaimer in the
   49.15 + * documentation and/or other materials provided with the distribution.
   49.16 + *
   49.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   49.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   49.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   49.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   49.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   49.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   49.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   49.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   49.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   49.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   49.27 + */
   49.28 +
   49.29 +package de.uapcore.lightpit.viewmodel
   49.30 +
   49.31 +import de.uapcore.lightpit.entities.Component
   49.32 +import de.uapcore.lightpit.entities.Project
   49.33 +import de.uapcore.lightpit.entities.Version
   49.34 +
   49.35 +class NavMenuEntry(
   49.36 +    val level: Int,
   49.37 +    val caption: String,
   49.38 +    val href: String,
   49.39 +    val title: String = "",
   49.40 +    val active: Boolean = false,
   49.41 +    val resolveCaption: Boolean = false,
   49.42 +    val iconColor: String? = null
   49.43 +) {
   49.44 +    val iconUseCssClass = iconColor != null && !iconColor.startsWith("#")
   49.45 +}
   49.46 +
   49.47 +class NavMenu(val entries: List<NavMenuEntry>)
   49.48 +
   49.49 +fun projectNavMenu(
   49.50 +    projects: List<Project>,
   49.51 +    versions: List<Version> = emptyList(),
   49.52 +    components: List<Component> = emptyList(),
   49.53 +    selectedProject: Project? = null,
   49.54 +    selectedVersion: Version? = null,
   49.55 +    selectedComponent: Component? = null
   49.56 +) = NavMenu(
   49.57 +    sequence {
   49.58 +        val cnode = selectedComponent?.node ?: "-"
   49.59 +        val vnode = selectedVersion?.node ?: "-"
   49.60 +        for (project in projects) {
   49.61 +            val active = project == selectedProject
   49.62 +            yield(
   49.63 +                NavMenuEntry(
   49.64 +                    level = 0,
   49.65 +                    caption = project.name,
   49.66 +                    href = "projects/${project.node}",
   49.67 +                    active = active
   49.68 +                )
   49.69 +            )
   49.70 +            if (active) {
   49.71 +                yield(
   49.72 +                    NavMenuEntry(
   49.73 +                        level = 1,
   49.74 +                        caption = "navmenu.versions",
   49.75 +                        resolveCaption = true,
   49.76 +                        href = "projects/${project.node}/versions/"
   49.77 +                    )
   49.78 +                )
   49.79 +                yield(
   49.80 +                    NavMenuEntry(
   49.81 +                        level = 2,
   49.82 +                        caption = "navmenu.all",
   49.83 +                        resolveCaption = true,
   49.84 +                        href = "projects/${project.node}/issues/-/${cnode}/",
   49.85 +                        iconColor = "#000000"
   49.86 +                    )
   49.87 +                )
   49.88 +                for (version in versions) {
   49.89 +                    yield(
   49.90 +                        NavMenuEntry(
   49.91 +                            level = 2,
   49.92 +                            caption = version.name,
   49.93 +                            title = "version.status.${version.status}",
   49.94 +                            href = "projects/${project.node}/issues/${version.node}/${cnode}/",
   49.95 +                            iconColor = "version-${version.status}",
   49.96 +                            active = version == selectedVersion
   49.97 +                        )
   49.98 +                    )
   49.99 +                }
  49.100 +                yield(
  49.101 +                    NavMenuEntry(
  49.102 +                        level = 1,
  49.103 +                        caption = "navmenu.components",
  49.104 +                        resolveCaption = true,
  49.105 +                        href = "projects/${project.node}/components/"
  49.106 +                    )
  49.107 +                )
  49.108 +                yield(
  49.109 +                    NavMenuEntry(
  49.110 +                        level = 2,
  49.111 +                        caption = "navmenu.all",
  49.112 +                        resolveCaption = true,
  49.113 +                        href = "projects/${project.node}/issues/${vnode}/-/",
  49.114 +                        iconColor = "#000000"
  49.115 +                    )
  49.116 +                )
  49.117 +                for (component in components) {
  49.118 +                    yield(
  49.119 +                        NavMenuEntry(
  49.120 +                            level = 2,
  49.121 +                            caption = component.name,
  49.122 +                            href = "projects/${project.node}/issues/${vnode}/${component.node}/",
  49.123 +                            iconColor = "${component.color}",
  49.124 +                            active = component == selectedComponent
  49.125 +                        )
  49.126 +                    )
  49.127 +                }
  49.128 +            }
  49.129 +        }
  49.130 +    }.toList()
  49.131 +)
    50.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    50.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt	Fri Apr 02 11:59:14 2021 +0200
    50.3 @@ -0,0 +1,66 @@
    50.4 +/*
    50.5 + * Copyright 2021 Mike Becker. All rights reserved.
    50.6 + *
    50.7 + * Redistribution and use in source and binary forms, with or without
    50.8 + * modification, are permitted provided that the following conditions are met:
    50.9 + *
   50.10 + * 1. Redistributions of source code must retain the above copyright
   50.11 + * notice, this list of conditions and the following disclaimer.
   50.12 + *
   50.13 + * 2. Redistributions in binary form must reproduce the above copyright
   50.14 + * notice, this list of conditions and the following disclaimer in the
   50.15 + * documentation and/or other materials provided with the distribution.
   50.16 + *
   50.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   50.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   50.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   50.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   50.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   50.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   50.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   50.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   50.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   50.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   50.27 + */
   50.28 +
   50.29 +package de.uapcore.lightpit.viewmodel
   50.30 +
   50.31 +import de.uapcore.lightpit.entities.*
   50.32 +import de.uapcore.lightpit.types.VersionStatus
   50.33 +
   50.34 +class ProjectInfo(
   50.35 +    val project: Project,
   50.36 +    /**
   50.37 +     * List of versions, sorted by status descending.
   50.38 +     */
   50.39 +    var versions: List<Version>,
   50.40 +    var components: List<Component>,
   50.41 +    var issueSummary: IssueSummary
   50.42 +) {
   50.43 +    val latestVersion = versions.firstOrNull { it.status == VersionStatus.Released }
   50.44 +    val nextVersion = versions.findLast { !it.status.isReleased }
   50.45 +}
   50.46 +
   50.47 +class ProjectsView(
   50.48 +    val projects: List<ProjectInfo>
   50.49 +) : View()
   50.50 +
   50.51 +class ProjectDetails(
   50.52 +    val projectInfo: ProjectInfo,
   50.53 +    val issues: List<Issue>,
   50.54 +    val version: Version? = null,
   50.55 +    val component: Component? = null
   50.56 +) : View() {
   50.57 +    val issueSummary = IssueSummary()
   50.58 +    val versionInfo: VersionInfo?
   50.59 +
   50.60 +    init {
   50.61 +        issues.forEach(issueSummary::add)
   50.62 +        versionInfo = version?.let { VersionInfo(it, issues) }
   50.63 +    }
   50.64 +}
   50.65 +
   50.66 +class ProjectEditView(
   50.67 +    val project: Project,
   50.68 +    val users: List<User>
   50.69 +) : EditView()
    51.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    51.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Users.kt	Fri Apr 02 11:59:14 2021 +0200
    51.3 @@ -0,0 +1,36 @@
    51.4 +/*
    51.5 + * Copyright 2021 Mike Becker. All rights reserved.
    51.6 + *
    51.7 + * Redistribution and use in source and binary forms, with or without
    51.8 + * modification, are permitted provided that the following conditions are met:
    51.9 + *
   51.10 + * 1. Redistributions of source code must retain the above copyright
   51.11 + * notice, this list of conditions and the following disclaimer.
   51.12 + *
   51.13 + * 2. Redistributions in binary form must reproduce the above copyright
   51.14 + * notice, this list of conditions and the following disclaimer in the
   51.15 + * documentation and/or other materials provided with the distribution.
   51.16 + *
   51.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   51.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   51.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   51.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   51.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   51.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   51.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   51.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   51.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   51.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   51.27 + */
   51.28 +
   51.29 +package de.uapcore.lightpit.viewmodel
   51.30 +
   51.31 +import de.uapcore.lightpit.entities.User
   51.32 +
   51.33 +class UsersView(
   51.34 +    val users: List<User>
   51.35 +) : View()
   51.36 +
   51.37 +class UserEditView(
   51.38 +    val user: User
   51.39 +) : EditView()
    52.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    52.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.kt	Fri Apr 02 11:59:14 2021 +0200
    52.3 @@ -0,0 +1,76 @@
    52.4 +/*
    52.5 + * Copyright 2021 Mike Becker. All rights reserved.
    52.6 + *
    52.7 + * Redistribution and use in source and binary forms, with or without
    52.8 + * modification, are permitted provided that the following conditions are met:
    52.9 + *
   52.10 + * 1. Redistributions of source code must retain the above copyright
   52.11 + * notice, this list of conditions and the following disclaimer.
   52.12 + *
   52.13 + * 2. Redistributions in binary form must reproduce the above copyright
   52.14 + * notice, this list of conditions and the following disclaimer in the
   52.15 + * documentation and/or other materials provided with the distribution.
   52.16 + *
   52.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   52.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   52.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   52.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   52.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   52.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   52.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   52.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   52.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   52.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   52.27 + */
   52.28 +
   52.29 +package de.uapcore.lightpit.viewmodel
   52.30 +
   52.31 +import de.uapcore.lightpit.entities.Issue
   52.32 +import de.uapcore.lightpit.entities.Version
   52.33 +import de.uapcore.lightpit.types.VersionStatus
   52.34 +
   52.35 +class VersionInfo(
   52.36 +    val version: Version,
   52.37 +    val issues: List<Issue>
   52.38 +) {
   52.39 +    val reportedTotal = IssueSummary()
   52.40 +    val resolvedTotal = IssueSummary()
   52.41 +    val reported: List<Issue>
   52.42 +    val resolved: List<Issue>
   52.43 +
   52.44 +    init {
   52.45 +        val reported = mutableListOf<Issue>()
   52.46 +        val resolved = mutableListOf<Issue>()
   52.47 +        for (issue in issues) {
   52.48 +            if (issue.affectedVersions.contains(version)) {
   52.49 +                reportedTotal.add(issue)
   52.50 +                reported.add(issue)
   52.51 +            }
   52.52 +            if (issue.resolvedVersions.contains(version)) {
   52.53 +                resolvedTotal.add(issue)
   52.54 +                resolved.add(issue)
   52.55 +            }
   52.56 +        }
   52.57 +        this.reported = reported
   52.58 +        this.resolved = resolved
   52.59 +    }
   52.60 +}
   52.61 +
   52.62 +class VersionSummary(
   52.63 +    val version: Version
   52.64 +) {
   52.65 +    val reportedTotal = IssueSummary()
   52.66 +    val resolvedTotal = IssueSummary()
   52.67 +}
   52.68 +
   52.69 +class VersionsView(
   52.70 +    val projectInfo: ProjectInfo,
   52.71 +    val versionInfos: List<VersionSummary>
   52.72 +) : View()
   52.73 +
   52.74 +class VersionEditView(
   52.75 +    val projectInfo: ProjectInfo,
   52.76 +    val version: Version
   52.77 +) : EditView() {
   52.78 +    val versionStatus = VersionStatus.values()
   52.79 +}
    53.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    53.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt	Fri Apr 02 11:59:14 2021 +0200
    53.3 @@ -0,0 +1,31 @@
    53.4 +/*
    53.5 + * Copyright 2021 Mike Becker. All rights reserved.
    53.6 + *
    53.7 + * Redistribution and use in source and binary forms, with or without
    53.8 + * modification, are permitted provided that the following conditions are met:
    53.9 + *
   53.10 + * 1. Redistributions of source code must retain the above copyright
   53.11 + * notice, this list of conditions and the following disclaimer.
   53.12 + *
   53.13 + * 2. Redistributions in binary form must reproduce the above copyright
   53.14 + * notice, this list of conditions and the following disclaimer in the
   53.15 + * documentation and/or other materials provided with the distribution.
   53.16 + *
   53.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   53.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   53.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   53.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   53.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   53.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   53.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   53.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   53.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   53.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   53.27 + */
   53.28 +
   53.29 +package de.uapcore.lightpit.viewmodel
   53.30 +
   53.31 +abstract class View
   53.32 +abstract class EditView : View() {
   53.33 +    var errorText: String? = null
   53.34 +}
    54.1 --- a/src/main/resources/localization/strings.properties	Sat Jan 23 14:47:59 2021 +0100
    54.2 +++ b/src/main/resources/localization/strings.properties	Fri Apr 02 11:59:14 2021 +0200
    54.3 @@ -33,6 +33,7 @@
    54.4  button.language.submit = Switch language
    54.5  button.okay=OK
    54.6  button.project.create=New Project
    54.7 +button.project.edit=Edit Project
    54.8  button.user.create=Add Developer
    54.9  button.version.create=New Version
   54.10  button.version.edit=Edit Version
   54.11 @@ -80,7 +81,6 @@
   54.12  issue.status=Status
   54.13  issue.subject=Subject
   54.14  issue.updated=Updated
   54.15 -issue.without-version=No Assigned Version
   54.16  issues.active=In Progress
   54.17  issues.done=Done
   54.18  issues.open=Open
   54.19 @@ -94,7 +94,6 @@
   54.20  menu.users=Developer
   54.21  navmenu.all=all
   54.22  navmenu.components=Components
   54.23 -navmenu.unassigned=unassigned
   54.24  navmenu.versions=Versions
   54.25  no-projects=Welcome to LightPIT. Start off by creating a new project!
   54.26  no-users=No developers have been configured yet.
   54.27 @@ -118,6 +117,7 @@
   54.28  user.lastname=Last Name
   54.29  user.mail=E-Mail
   54.30  username=User Name
   54.31 +validation.username.unique=Username is already taken.
   54.32  version.latest=Latest Version
   54.33  version.next=Next Version
   54.34  version.status.Deprecated=Deprecated
   54.35 @@ -126,4 +126,4 @@
   54.36  version.status.Released=Released
   54.37  version.status.Unreleased=Unreleased
   54.38  version.status=Status
   54.39 -version=Version
   54.40 +version=Version
   54.41 \ No newline at end of file
    55.1 --- a/src/main/resources/localization/strings_de.properties	Sat Jan 23 14:47:59 2021 +0100
    55.2 +++ b/src/main/resources/localization/strings_de.properties	Fri Apr 02 11:59:14 2021 +0200
    55.3 @@ -33,6 +33,7 @@
    55.4  button.language.submit = Sprache ausw\u00e4hlen
    55.5  button.okay=OK
    55.6  button.project.create=Neues Projekt
    55.7 +button.project.edit=Projekt Bearbeiten
    55.8  button.user.create=Neuer Entwickler
    55.9  button.version.create=Neue Version
   55.10  button.version.edit=Version Bearbeiten
   55.11 @@ -80,7 +81,6 @@
   55.12  issue.status=Status
   55.13  issue.subject=Thema
   55.14  issue.updated=Aktualisiert
   55.15 -issue.without-version=Keine Version zugeordnet
   55.16  issues.active=In Arbeit
   55.17  issues.done=Erledigt
   55.18  issues.open=Offen
   55.19 @@ -94,7 +94,6 @@
   55.20  menu.users=Entwickler
   55.21  navmenu.all=Alle
   55.22  navmenu.components=Komponenten
   55.23 -navmenu.unassigned=Nicht Zugewiesen
   55.24  navmenu.versions=Versionen
   55.25  no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
   55.26  no-users=Bislang wurden keine Entwickler hinterlegt.
   55.27 @@ -118,6 +117,7 @@
   55.28  user.lastname=Nachname
   55.29  user.mail=E-Mail
   55.30  username=Benutzername
   55.31 +validation.username.unique=Der Benutzername wird bereits verwendet.
   55.32  version.latest=Neuste Version
   55.33  version.next=N\u00e4chste Version
   55.34  version.status.Deprecated=Veraltet
    56.1 --- a/src/main/webapp/WEB-INF/jsp/component-form.jsp	Sat Jan 23 14:47:59 2021 +0100
    56.2 +++ b/src/main/webapp/WEB-INF/jsp/component-form.jsp	Fri Apr 02 11:59:14 2021 +0200
    56.3 @@ -32,7 +32,7 @@
    56.4  <c:set var="component" scope="page" value="${viewmodel.component}"/>
    56.5  <c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
    56.6  
    56.7 -<form action="./projects/commit-component" method="post">
    56.8 +<form action="./projects/${project.node}/components/-/commit" method="post">
    56.9      <table class="formtable" style="width: 70ch">
   56.10          <colgroup>
   56.11              <col>
   56.12 @@ -43,7 +43,7 @@
   56.13              <th><fmt:message key="project"/></th>
   56.14              <td>
   56.15                  <c:out value="${project.name}" />
   56.16 -                <input type="hidden" name="pid" value="${project.id}" />
   56.17 +                <input type="hidden" name="projectid" value="${project.id}" />
   56.18              </td>
   56.19          </tr>
   56.20          <tr>
    57.1 --- a/src/main/webapp/WEB-INF/jsp/components.jsp	Sat Jan 23 14:47:59 2021 +0100
    57.2 +++ b/src/main/webapp/WEB-INF/jsp/components.jsp	Fri Apr 02 11:59:14 2021 +0200
    57.3 @@ -35,8 +35,8 @@
    57.4  <%@include file="../jspf/project-header.jspf"%>
    57.5  
    57.6  <div id="tool-area">
    57.7 -    <a href="./projects/${project.node}/create-component" class="button"><fmt:message key="button.component.create"/></a>
    57.8 -    <a href="./projects/${project.node}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
    57.9 +    <a href="./projects/${project.node}/components/-/create" class="button"><fmt:message key="button.component.create"/></a>
   57.10 +    <a href="./projects/${project.node}/issues/-/-/-/create" class="button"><fmt:message key="button.issue.create"/></a>
   57.11  </div>
   57.12  
   57.13  <h2><fmt:message key="progress" /></h2>
   57.14 @@ -75,7 +75,7 @@
   57.15              <td rowspan="2" style="width: 2em;"><a href="./projects/${project.node}/components/${componentInfo.component.node}/edit">&#x270e;</a></td>
   57.16              <td rowspan="2">
   57.17                  <div class="navmenu-icon" style="background-color: ${componentInfo.component.color}"></div>
   57.18 -                <a href="./projects/${project.node}/${componentInfo.component.node}/all-versions/issues/">
   57.19 +                <a href="./projects/${project.node}/issues/-/${componentInfo.component.node}/">
   57.20                      <c:out value="${componentInfo.component.name}"/>
   57.21                  </a>
   57.22              </td>
    58.1 --- a/src/main/webapp/WEB-INF/jsp/error.jsp	Sat Jan 23 14:47:59 2021 +0100
    58.2 +++ b/src/main/webapp/WEB-INF/jsp/error.jsp	Fri Apr 02 11:59:14 2021 +0200
    58.3 @@ -26,14 +26,13 @@
    58.4  --%>
    58.5  <%@page pageEncoding="UTF-8" %>
    58.6  <%@page import="de.uapcore.lightpit.Constants" %>
    58.7 -<%@page import="de.uapcore.lightpit.modules.ErrorModule" %>
    58.8  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    58.9  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   58.10  <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
   58.11  
   58.12  <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/>
   58.13  <c:set scope="page" var="errorCode" value="${requestScope['javax.servlet.error.status_code']}"/>
   58.14 -<c:set scope="page" var="returnLink" value="${requestScope[ErrorModule.REQ_ATTR_RETURN_LINK]}"/>
   58.15 +<c:set scope="page" var="returnLink" value="${requestScope[Constants.REQ_ATTR_REFERER]}"/>
   58.16  
   58.17  <div id="error-page">
   58.18      <h1><fmt:message key="error.headline"/></h1>
    59.1 --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat Jan 23 14:47:59 2021 +0100
    59.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Fri Apr 02 11:59:14 2021 +0200
    59.3 @@ -29,10 +29,15 @@
    59.4  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    59.5  
    59.6  <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueEditView" scope="request"/>
    59.7 +
    59.8  <c:set var="issue" scope="page" value="${viewmodel.issue}" />
    59.9 +<c:set var="project" scope="page" value="${viewmodel.project}"/>
   59.10 +<c:set var="component" scope="page" value="${viewmodel.component}"/>
   59.11 +<c:set var="version" scope="page" value="${viewmodel.version}"/>
   59.12  
   59.13 -<%-- TODO: change to ./issues/commit --%>
   59.14 -<form action="./projects/commit-issue" method="post">
   59.15 +<c:set var="issuesHref" value="./projects/${project.node}/issues/${empty version ? '-' : version.node }/${empty component ? '-' : component.node}/"/>
   59.16 +
   59.17 +<form action="${issuesHref}-/commit-issue" method="post">
   59.18      <table class="formtable fullwidth">
   59.19          <colgroup>
   59.20              <col>
   59.21 @@ -49,12 +54,12 @@
   59.22              <th><fmt:message key="project"/></th>
   59.23              <td>
   59.24                  <c:choose>
   59.25 -                    <c:when test="${not empty issue.project}">
   59.26 +                    <c:when test="${issue.project.id ge 0}">
   59.27                          <c:out value="${issue.project.name}" />
   59.28 -                        <input type="hidden" name="pid" value="${issue.project.id}" />
   59.29 +                        <input type="hidden" name="project" value="${issue.project.id}" />
   59.30                      </c:when>
   59.31                      <c:otherwise>
   59.32 -                        <select name="pid" required>
   59.33 +                        <select name="project" required>
   59.34                              <c:forEach var="project" items="${viewmodel.projects}">
   59.35                                  <option value="${project.id}">
   59.36                                      <c:out value="${project.name}" />
   59.37 @@ -179,13 +184,12 @@
   59.38                  <label for="create-another"><fmt:message key="button.issue.create.another"/> </label>
   59.39                  <input type="hidden" name="id" value="${issue.id}"/>
   59.40                  <c:if test="${issue.id ge 0}">
   59.41 -                <a href="./projects/${issue.project.node}/issues/${issue.id}/view" class="button">
   59.42 +                <a href="${issuesHref}${issue.id}" class="button">
   59.43                      <fmt:message key="button.cancel"/>
   59.44                  </a>
   59.45                  </c:if>
   59.46                  <c:if test="${issue.id lt 0}">
   59.47 -                    <%-- TODO: fix #14 --%>
   59.48 -                    <a href="./projects/${issue.project.node}/all-components/all-versions/issues/" class="button">
   59.49 +                    <a href="${issuesHref}" class="button">
   59.50                          <fmt:message key="button.cancel"/>
   59.51                      </a>
   59.52                  </c:if>
    60.1 --- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Sat Jan 23 14:47:59 2021 +0100
    60.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Fri Apr 02 11:59:14 2021 +0200
    60.3 @@ -29,8 +29,14 @@
    60.4  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    60.5  
    60.6  <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueDetailView" scope="request"/>
    60.7 +
    60.8 +<c:set var="project" scope="page" value="${viewmodel.project}"/>
    60.9 +<c:set var="component" scope="page" value="${viewmodel.component}"/>
   60.10 +<c:set var="version" scope="page" value="${viewmodel.version}"/>
   60.11  <c:set var="issue" scope="page" value="${viewmodel.issue}" />
   60.12  
   60.13 +<c:set var="issuesHref" scope="page" value="./projects/${project.node}/issues/${empty version ? '-' : version.node }/${empty component ? '-' : component.node}/"/>
   60.14 +
   60.15  <table class="formtable fullwidth">
   60.16      <colgroup>
   60.17          <col>
   60.18 @@ -137,11 +143,10 @@
   60.19      <tfoot>
   60.20      <tr>
   60.21          <td colspan="2">
   60.22 -            <%-- TODO: fix #14 --%>
   60.23 -            <a href="./projects/${issue.project.node}/all-components/all-versions/issues/" class="button">
   60.24 +            <a href="${issuesHref}" class="button">
   60.25                  <fmt:message key="button.cancel"/>
   60.26              </a>
   60.27 -            <a href="./projects/${issue.project.node}/issues/${issue.id}/edit" class="button submit">
   60.28 +            <a href="${issuesHref}${issue.id}/edit" class="button submit">
   60.29                  <fmt:message key="button.issue.edit"/>
   60.30              </a>
   60.31          </td>
   60.32 @@ -152,7 +157,7 @@
   60.33  <hr class="comments-separator"/>
   60.34  <h2><fmt:message key="issue.comments"/></h2>
   60.35  <c:if test="${viewmodel.issue.id ge 0}">
   60.36 -<form id="comment-form" action="./projects/commit-issue-comment" method="post">
   60.37 +<form id="comment-form" action="${issuesHref}${issue.id}/comment" method="post">
   60.38      <table class="formtable fullwidth">
   60.39          <tbody>
   60.40              <tr>
   60.41 @@ -162,7 +167,6 @@
   60.42          <tfoot>
   60.43              <tr>
   60.44                  <td>
   60.45 -                    <input type="hidden" name="issueid" value="${issue.id}"/>
   60.46                      <button type="submit"><fmt:message key="button.comment"/></button>
   60.47                  </td>
   60.48              </tr>
    61.1 --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat Jan 23 14:47:59 2021 +0100
    61.2 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Fri Apr 02 11:59:14 2021 +0200
    61.3 @@ -24,37 +24,28 @@
    61.4  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    61.5  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    61.6  --%>
    61.7 -<%@page pageEncoding="UTF-8" import="de.uapcore.lightpit.viewmodel.ProjectView" %>
    61.8  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    61.9  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   61.10  <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
   61.11  
   61.12 -<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectDetailsView" scope="request" />
   61.13 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectDetails" scope="request" />
   61.14  
   61.15  <c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
   61.16 -<c:set var="component" scope="page" value="${viewmodel.componentFilter}"/>
   61.17 +<c:set var="component" scope="page" value="${viewmodel.component}"/>
   61.18 +<c:set var="version" scope="page" value="${viewmodel.version}"/>
   61.19  <%@include file="../jspf/project-header.jspf"%>
   61.20  
   61.21  <div id="tool-area">
   61.22 -    <c:remove var="createIssueParams"/>
   61.23 -    <c:if test="${viewmodel.versionFilter.id gt 0}">
   61.24 -        <c:set var="createIssueParams">&vid=${viewmodel.versionFilter.id}</c:set>
   61.25 +    <a href="./projects/${project.node}/issues/${empty version ? '-' : version.node}/${empty component ? '-' : component.node}/-/create" class="button"><fmt:message key="button.issue.create"/></a>
   61.26 +    <a href="./projects/${project.node}/edit" class="button"><fmt:message key="button.project.edit"/></a>
   61.27 +    <c:if test="${not empty version}">
   61.28 +        <a href="./projects/${project.node}/versions/${version.node}/edit" class="button"><fmt:message key="button.version.edit"/></a>
   61.29      </c:if>
   61.30 -    <c:if test="${viewmodel.componentFilter.id gt 0}">
   61.31 -        <c:set var="createIssueParams">${createIssueParams}&cid=${viewmodel.componentFilter.id}</c:set>
   61.32 +    <a href="./projects/${project.node}/versions/-/create" class="button"><fmt:message key="button.version.create"/></a>
   61.33 +    <c:if test="${not empty component}">
   61.34 +        <a href="./projects/${project.node}/components/${component.node}/edit" class="button"><fmt:message key="button.component.edit"/></a>
   61.35      </c:if>
   61.36 -    <c:if test="${not empty createIssueParams}">
   61.37 -        <c:set var="createIssueParams">?${fn:substringAfter(createIssueParams, "&")}</c:set>
   61.38 -    </c:if>
   61.39 -    <a href="./projects/${project.node}/create-issue${createIssueParams}" class="button"><fmt:message key="button.issue.create"/></a>
   61.40 -    <c:if test="${viewmodel.versionFilter.id gt 0}">
   61.41 -        <a href="./projects/${project.node}/versions/${viewmodel.versionFilter.node}/edit" class="button"><fmt:message key="button.version.edit"/></a>
   61.42 -    </c:if>
   61.43 -    <a href="./projects/${project.node}/create-version" class="button"><fmt:message key="button.version.create"/></a>
   61.44 -    <c:if test="${viewmodel.componentFilter.id gt 0}">
   61.45 -        <a href="./projects/${project.node}/components/${viewmodel.componentFilter.node}/edit" class="button"><fmt:message key="button.component.edit"/></a>
   61.46 -    </c:if>
   61.47 -    <a href="./projects/${project.node}/create-component" class="button"><fmt:message key="button.component.create"/></a>
   61.48 +    <a href="./projects/${project.node}/components/-/create" class="button"><fmt:message key="button.component.create"/></a>
   61.49  </div>
   61.50  
   61.51  <h2><fmt:message key="progress" /></h2>
   61.52 @@ -63,24 +54,19 @@
   61.53  <%@include file="../jspf/issue-summary.jspf"%>
   61.54  
   61.55  <c:choose>
   61.56 -    <c:when test="${viewmodel.versionFilter eq ProjectView.NO_VERSION or viewmodel.versionFilter eq ProjectView.ALL_VERSIONS}">
   61.57 +    <c:when test="${empty viewmodel.versionInfo}">
   61.58          <h2>
   61.59 -            <c:if test="${viewmodel.versionFilter eq ProjectView.NO_VERSION}">
   61.60 -                <fmt:message key="issue.without-version" />
   61.61 -            </c:if>
   61.62 -            <c:if test="${viewmodel.versionFilter ne ProjectView.NO_VERSION}">
   61.63 -                <fmt:message key="issues" />
   61.64 -            </c:if>
   61.65 +            <fmt:message key="issues" />
   61.66          </h2>
   61.67 -        <c:set var="summary" value="${viewmodel.projectDetails.issueSummary}"/>
   61.68 -        <c:set var="issues" value="${viewmodel.projectDetails.issues}"/>
   61.69 +        <c:set var="summary" value="${viewmodel.issueSummary}"/>
   61.70 +        <c:set var="issues" value="${viewmodel.issues}"/>
   61.71          <%@include file="../jspf/issue-summary.jspf"%>
   61.72          <c:if test="${not empty issues}">
   61.73              <%@include file="../jspf/issue-list.jspf"%>
   61.74          </c:if>
   61.75      </c:when>
   61.76      <c:otherwise>
   61.77 -        <c:set var="versionInfo" value="${viewmodel.projectDetails.versionInfo}"/>
   61.78 +        <c:set var="versionInfo" value="${viewmodel.versionInfo}"/>
   61.79          <h2>
   61.80              <fmt:message key="version" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
   61.81          </h2>
    62.1 --- a/src/main/webapp/WEB-INF/jsp/project-form.jsp	Sat Jan 23 14:47:59 2021 +0100
    62.2 +++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp	Fri Apr 02 11:59:14 2021 +0200
    62.3 @@ -31,7 +31,7 @@
    62.4  <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectEditView" scope="request" />
    62.5  <c:set var="project" scope="page" value="${viewmodel.project}"/>
    62.6  
    62.7 -<form action="./projects/commit" method="post">
    62.8 +<form action="./projects/-/commit" method="post">
    62.9      <table class="formtable">
   62.10          <colgroup>
   62.11              <col>
   62.12 @@ -77,7 +77,7 @@
   62.13          <tfoot>
   62.14          <tr>
   62.15              <td colspan="2">
   62.16 -                <input type="hidden" name="pid" value="${project.id}"/>
   62.17 +                <input type="hidden" name="id" value="${project.id}"/>
   62.18                  <a href="./projects/" class="button">
   62.19                      <fmt:message key="button.cancel"/>
   62.20                  </a>
    63.1 --- a/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Sat Jan 23 14:47:59 2021 +0100
    63.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    63.3 @@ -1,104 +0,0 @@
    63.4 -<%--
    63.5 -DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    63.6 -
    63.7 -Copyright 2021 Mike Becker. All rights reserved.
    63.8 -
    63.9 -Redistribution and use in source and binary forms, with or without
   63.10 -modification, are permitted provided that the following conditions are met:
   63.11 -
   63.12 -1. Redistributions of source code must retain the above copyright
   63.13 -notice, this list of conditions and the following disclaimer.
   63.14 -
   63.15 -2. Redistributions in binary form must reproduce the above copyright
   63.16 -notice, this list of conditions and the following disclaimer in the
   63.17 -documentation and/or other materials provided with the distribution.
   63.18 -
   63.19 -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   63.20 -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   63.21 -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   63.22 -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   63.23 -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   63.24 -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   63.25 -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   63.26 -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   63.27 -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   63.28 -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   63.29 ---%>
   63.30 -<%@page pageEncoding="UTF-8"
   63.31 -        import="de.uapcore.lightpit.viewmodel.ProjectView"
   63.32 -        import="de.uapcore.lightpit.types.VersionStatus"
   63.33 -%>
   63.34 -<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   63.35 -<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   63.36 -
   63.37 -<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request"/>
   63.38 -
   63.39 -<c:forEach var="projectInfo" items="${viewmodel.projectList}">
   63.40 -    <c:set var="isActive" value="${viewmodel.projectInfo.project eq projectInfo.project}" />
   63.41 -    <div class="menuEntry level-0" <c:if test="${isActive}">data-active</c:if> >
   63.42 -        <a href="projects/${projectInfo.project.node}/versions/">
   63.43 -            <c:out value="${projectInfo.project.name}"/>
   63.44 -        </a>
   63.45 -    </div>
   63.46 -    <c:if test="${isActive}">
   63.47 -        <!-- VERSIONS -->
   63.48 -        <c:set var="componentNode" value="${not empty viewmodel.componentFilter ? viewmodel.componentFilter.node : 'all-components'}"/>
   63.49 -        <div class="menuEntry level-1" <c:if test="${viewmodel.selectedPage eq ProjectView.SELECTED_PAGE_VERSIONS}">data-active</c:if> >
   63.50 -            <a href="projects/${projectInfo.project.node}/versions/">
   63.51 -                <fmt:message key="navmenu.versions"/>
   63.52 -            </a>
   63.53 -        </div>
   63.54 -        <div class="menuEntry level-2" <c:if test="${viewmodel.versionFilter eq ProjectView.ALL_VERSIONS}">data-active</c:if>>
   63.55 -            <div class="navmenu-icon" style="background: black"></div>
   63.56 -            <a href="projects/${projectInfo.project.node}/${componentNode}/all-versions/issues/">
   63.57 -                <fmt:message key="navmenu.all" />
   63.58 -            </a>
   63.59 -        </div>
   63.60 -        <div class="menuEntry level-2" <c:if test="${viewmodel.versionFilter eq ProjectView.NO_VERSION}">data-active</c:if>>
   63.61 -            <div class="navmenu-icon" style="background: black"></div>
   63.62 -            <a href="projects/${projectInfo.project.node}/${componentNode}/no-version/issues/">
   63.63 -                <fmt:message key="navmenu.unassigned" />
   63.64 -            </a>
   63.65 -        </div>
   63.66 -        <c:forEach var="version" items="${viewmodel.projectInfo.versions}">
   63.67 -            <c:set var="isVersionActive" value="${viewmodel.versionFilter eq version}" />
   63.68 -            <c:if test="${version.status ne VersionStatus.Deprecated or isVersionActive}">
   63.69 -            <div class="menuEntry level-2" <c:if test="${isVersionActive}">data-active</c:if>
   63.70 -                    title="<fmt:message key="version.status.${version.status}" />">
   63.71 -                <div class="navmenu-icon version-${version.status}"></div>
   63.72 -                <a href="projects/${projectInfo.project.node}/${componentNode}/${version.node}/issues/">
   63.73 -                    <c:out value="${version.name}"/>
   63.74 -                </a>
   63.75 -            </div>
   63.76 -            </c:if>
   63.77 -        </c:forEach>
   63.78 -        <!-- COMPONENTS -->
   63.79 -        <c:set var="versionNode" value="${not empty viewmodel.versionFilter ? viewmodel.versionFilter.node : 'all-versions'}"/>
   63.80 -        <div class="menuEntry level-1" <c:if test="${viewmodel.selectedPage eq ProjectView.SELECTED_PAGE_COMPONENTS}">data-active</c:if>>
   63.81 -            <a href="projects/${projectInfo.project.node}/components/">
   63.82 -                <fmt:message key="navmenu.components"/>
   63.83 -            </a>
   63.84 -        </div>
   63.85 -        <div class="menuEntry level-2" <c:if test="${viewmodel.componentFilter eq ProjectView.ALL_COMPONENTS}">data-active</c:if>>
   63.86 -            <div class="navmenu-icon" style="background: black"></div>
   63.87 -            <a href="projects/${projectInfo.project.node}/all-components/${versionNode}/issues/">
   63.88 -                <fmt:message key="navmenu.all" />
   63.89 -            </a>
   63.90 -        </div>
   63.91 -        <div class="menuEntry level-2"  <c:if test="${viewmodel.componentFilter eq ProjectView.NO_COMPONENT}">data-active</c:if>>
   63.92 -            <div class="navmenu-icon" style="background: black"></div>
   63.93 -            <a href="projects/${projectInfo.project.node}/no-component/${versionNode}/issues/">
   63.94 -                <fmt:message key="navmenu.unassigned" />
   63.95 -            </a>
   63.96 -        </div>
   63.97 -        <c:forEach var="component" items="${viewmodel.projectInfo.components}">
   63.98 -            <c:set var="isComponentActive" value="${viewmodel.componentFilter eq component}" />
   63.99 -            <div class="menuEntry level-2" <c:if test="${isComponentActive}">data-active</c:if> >
  63.100 -                <div class="navmenu-icon" style="background-color: ${component.color}"></div>
  63.101 -                <a href="projects/${projectInfo.project.node}/${component.node}/${versionNode}/issues/">
  63.102 -                    <c:out value="${component.name}"/>
  63.103 -                </a>
  63.104 -            </div>
  63.105 -        </c:forEach>
  63.106 -    </c:if>
  63.107 -</c:forEach>
    64.1 --- a/src/main/webapp/WEB-INF/jsp/projects.jsp	Sat Jan 23 14:47:59 2021 +0100
    64.2 +++ b/src/main/webapp/WEB-INF/jsp/projects.jsp	Fri Apr 02 11:59:14 2021 +0200
    64.3 @@ -28,19 +28,19 @@
    64.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    64.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    64.6  
    64.7 -<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request"/>
    64.8 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectsView" scope="request"/>
    64.9  
   64.10 -<c:if test="${empty viewmodel.projectList}">
   64.11 +<c:if test="${empty viewmodel.projects}">
   64.12      <div class="info-box">
   64.13          <fmt:message key="no-projects"/>
   64.14      </div>
   64.15  </c:if>
   64.16  
   64.17  <div id="tool-area">
   64.18 -    <a href="./projects/create" class="button"><fmt:message key="button.project.create"/></a>
   64.19 +    <a href="./projects/-/create" class="button"><fmt:message key="button.project.create"/></a>
   64.20  </div>
   64.21  
   64.22 -<c:if test="${not empty viewmodel.projectList}">
   64.23 +<c:if test="${not empty viewmodel.projects}">
   64.24      <table id="project-list" class="datatable medskip">
   64.25          <colgroup>
   64.26              <col>
   64.27 @@ -65,11 +65,11 @@
   64.28          </tr>
   64.29          </thead>
   64.30          <tbody>
   64.31 -        <c:forEach var="projectInfo" items="${viewmodel.projectList}">
   64.32 +        <c:forEach var="projectInfo" items="${viewmodel.projects}">
   64.33              <c:set var="project" scope="page" value="${projectInfo.project}"/>
   64.34              <tr class="nowrap">
   64.35                  <td style="width: 2em;"><a href="./projects/${project.node}/edit">&#x270e;</a></td>
   64.36 -                <td><a href="./projects/${project.node}/versions/"><c:out value="${project.name}"/></a>
   64.37 +                <td><a href="./projects/${project.node}"><c:out value="${project.name}"/></a>
   64.38                  </td>
   64.39                  <td>
   64.40                      <c:if test="${not empty project.repoUrl}">
   64.41 @@ -79,12 +79,12 @@
   64.42                  </td>
   64.43                  <td class="hright">
   64.44                      <c:if test="${not empty projectInfo.latestVersion}">
   64.45 -                        <a href="./projects/${project.node}/all-components/${projectInfo.latestVersion.node}/issues/"><c:out value="${projectInfo.latestVersion.name}"/></a>
   64.46 +                        <a href="./projects/${project.node}/issues/${projectInfo.latestVersion.node}/-/"><c:out value="${projectInfo.latestVersion.name}"/></a>
   64.47                      </c:if>
   64.48                  </td>
   64.49                  <td class="hright">
   64.50                      <c:if test="${not empty projectInfo.nextVersion}">
   64.51 -                        <a href="./projects/${project.node}/all-components/${projectInfo.nextVersion.node}/issues/"><c:out value="${projectInfo.nextVersion.name}"/></a>
   64.52 +                        <a href="./projects/${project.node}/issues/${projectInfo.nextVersion.node}/-/"><c:out value="${projectInfo.nextVersion.name}"/></a>
   64.53                      </c:if>
   64.54                  </td>
   64.55                  <td class="hright">${projectInfo.issueSummary.open}</td>
    65.1 --- a/src/main/webapp/WEB-INF/jsp/site.jsp	Sat Jan 23 14:47:59 2021 +0100
    65.2 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Fri Apr 02 11:59:14 2021 +0200
    65.3 @@ -79,8 +79,8 @@
    65.4              <fmt:message key="menu.projects"/>
    65.5          </a>
    65.6      </div>
    65.7 -    <div class="menuEntry" <c:if test="${fn:startsWith(requestPath, '/teams/')}">data-active</c:if> >
    65.8 -        <a href="teams/">
    65.9 +    <div class="menuEntry" <c:if test="${fn:startsWith(requestPath, '/users/')}">data-active</c:if> >
   65.10 +        <a href="users/">
   65.11              <fmt:message key="menu.users"/>
   65.12          </a>
   65.13      </div>
   65.14 @@ -93,7 +93,7 @@
   65.15  <div>
   65.16      <c:if test="${not empty navMenu}">
   65.17          <div id="sideMenu">
   65.18 -            <c:import url="${navMenu}"/>
   65.19 +            <%@include file="../jspf/navmenu.jspf"%>
   65.20          </div>
   65.21      </c:if>
   65.22      <div id="content-area" <c:if test="${not empty navMenu}">class="sidebar-spacing"</c:if>>
    66.1 --- a/src/main/webapp/WEB-INF/jsp/user-form.jsp	Sat Jan 23 14:47:59 2021 +0100
    66.2 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp	Fri Apr 02 11:59:14 2021 +0200
    66.3 @@ -28,10 +28,10 @@
    66.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    66.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    66.6  
    66.7 -<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersEditView" scope="request"/>
    66.8 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UserEditView" scope="request"/>
    66.9  <c:set var="user" scope="page" value="${viewmodel.user}" />
   66.10  
   66.11 -<form action="./teams/commit" method="post">
   66.12 +<form action="./users/-/commit" method="post">
   66.13      <table class="formtable">
   66.14          <colgroup>
   66.15              <col>
   66.16 @@ -57,10 +57,19 @@
   66.17          </tr>
   66.18          </tbody>
   66.19          <tfoot>
   66.20 +        <c:if test="${not empty viewmodel.errorText}">
   66.21 +        <tr>
   66.22 +            <td colspan="2">
   66.23 +                <div class="error-box">
   66.24 +                    <fmt:message key="${viewmodel.errorText}"/>
   66.25 +                </div>
   66.26 +            </td>
   66.27 +        </tr>
   66.28 +        </c:if>
   66.29          <tr>
   66.30              <td colspan="2">
   66.31                  <input type="hidden" name="userid" value="${user.id}"/>
   66.32 -                <a href="./teams/" class="button">
   66.33 +                <a href="./users/" class="button">
   66.34                      <fmt:message key="button.cancel"/>
   66.35                  </a>
   66.36                  <button type="submit"><fmt:message key="button.okay"/></button>
    67.1 --- a/src/main/webapp/WEB-INF/jsp/users.jsp	Sat Jan 23 14:47:59 2021 +0100
    67.2 +++ b/src/main/webapp/WEB-INF/jsp/users.jsp	Fri Apr 02 11:59:14 2021 +0200
    67.3 @@ -37,7 +37,7 @@
    67.4  </c:if>
    67.5  
    67.6  <div id="tool-area">
    67.7 -    <a href="./teams/edit" class="button"><fmt:message key="button.user.create"/></a>
    67.8 +    <a href="./users/-/create" class="button"><fmt:message key="button.user.create"/></a>
    67.9  </div>
   67.10  
   67.11  <c:if test="${not empty viewmodel.users}">
   67.12 @@ -51,7 +51,7 @@
   67.13          <tbody>
   67.14          <c:forEach var="user" items="${viewmodel.users}">
   67.15              <tr>
   67.16 -                <td><a href="./teams/edit?id=${user.id}">&#x270e;</a></td>
   67.17 +                <td><a href="./users/${user.id}/edit">&#x270e;</a></td>
   67.18                  <td><c:out value="${user.displayname}"/></td>
   67.19              </tr>
   67.20          </c:forEach>
    68.1 --- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat Jan 23 14:47:59 2021 +0100
    68.2 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Fri Apr 02 11:59:14 2021 +0200
    68.3 @@ -32,7 +32,7 @@
    68.4  <c:set var="version" scope="page" value="${viewmodel.version}"/>
    68.5  <c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
    68.6  
    68.7 -<form action="./projects/commit-version" method="post">
    68.8 +<form action="./projects/${project.node}/versions/-/commit" method="post">
    68.9      <table class="formtable" style="width: 35ch">
   68.10          <colgroup>
   68.11              <col>
   68.12 @@ -43,7 +43,7 @@
   68.13              <th><fmt:message key="project"/></th>
   68.14              <td>
   68.15                  <c:out value="${project.name}" />
   68.16 -                <input type="hidden" name="pid" value="${project.id}" />
   68.17 +                <input type="hidden" name="projectid" value="${project.id}" />
   68.18              </td>
   68.19          </tr>
   68.20          <tr>
    69.1 --- a/src/main/webapp/WEB-INF/jsp/versions.jsp	Sat Jan 23 14:47:59 2021 +0100
    69.2 +++ b/src/main/webapp/WEB-INF/jsp/versions.jsp	Fri Apr 02 11:59:14 2021 +0200
    69.3 @@ -34,8 +34,8 @@
    69.4  <%@include file="../jspf/project-header.jspf"%>
    69.5  
    69.6  <div id="tool-area">
    69.7 -    <a href="./projects/${project.node}/create-version" class="button"><fmt:message key="button.version.create"/></a>
    69.8 -    <a href="./projects/${project.node}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
    69.9 +    <a href="./projects/${project.node}/versions/-/create" class="button"><fmt:message key="button.version.create"/></a>
   69.10 +    <a href="./projects/${project.node}/issues/-/-/-/create" class="button"><fmt:message key="button.issue.create"/></a>
   69.11  </div>
   69.12  
   69.13  <h2><fmt:message key="progress" /></h2>
   69.14 @@ -80,7 +80,7 @@
   69.15          <tr>
   69.16              <td rowspan="2" style="width: 2em;"><a href="./projects/${project.node}/versions/${versionInfo.version.node}/edit">&#x270e;</a></td>
   69.17              <td rowspan="2">
   69.18 -                <a href="./projects/${project.node}/all-components/${versionInfo.version.node}/issues/">
   69.19 +                <a href="./projects/${project.node}/issues/${versionInfo.version.node}/-/">
   69.20                      <c:out value="${versionInfo.version.name}"/>
   69.21                  </a>
   69.22                  <div class="version-tag version-${versionInfo.version.status}">
    70.1 --- a/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Sat Jan 23 14:47:59 2021 +0100
    70.2 +++ b/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Fri Apr 02 11:59:14 2021 +0200
    70.3 @@ -1,5 +1,7 @@
    70.4  <%--
    70.5  issues: List<Issue>
    70.6 +version: Version?
    70.7 +component: Component?
    70.8  --%>
    70.9  <table class="fullwidth datatable medskip">
   70.10      <colgroup>
   70.11 @@ -17,7 +19,7 @@
   70.12          <tr>
   70.13              <td>
   70.14                  <span class="phase-${issue.status.phase.number}">
   70.15 -                    <a href="./projects/${issue.project.node}/issues/${issue.id}/view">
   70.16 +                    <a href="./projects/${issue.project.node}/issues/${empty version ? '-' : version.node }/${empty component ? '-' : component.node}/${issue.id}">
   70.17                          #${issue.id}&nbsp;-&nbsp;<c:out value="${issue.subject}" />
   70.18                      </a>
   70.19                  </span>
    71.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    71.2 +++ b/src/main/webapp/WEB-INF/jspf/navmenu.jspf	Fri Apr 02 11:59:14 2021 +0200
    71.3 @@ -0,0 +1,55 @@
    71.4 +<%--
    71.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    71.6 +
    71.7 +Copyright 2021 Mike Becker. All rights reserved.
    71.8 +
    71.9 +Redistribution and use in source and binary forms, with or without
   71.10 +modification, are permitted provided that the following conditions are met:
   71.11 +
   71.12 +1. Redistributions of source code must retain the above copyright
   71.13 +notice, this list of conditions and the following disclaimer.
   71.14 +
   71.15 +2. Redistributions in binary form must reproduce the above copyright
   71.16 +notice, this list of conditions and the following disclaimer in the
   71.17 +documentation and/or other materials provided with the distribution.
   71.18 +
   71.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   71.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   71.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   71.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   71.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   71.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   71.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   71.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   71.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   71.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   71.29 +--%>
   71.30 +<%@page pageEncoding="UTF-8" %>
   71.31 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   71.32 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   71.33 +
   71.34 +<jsp:useBean id="navMenu" type="de.uapcore.lightpit.viewmodel.NavMenu" scope="request" />
   71.35 +
   71.36 +<c:forEach var="entry" items="${navMenu.entries}">
   71.37 +    <div class="menuEntry level-${entry.level}"
   71.38 +        <c:if test="${entry.active}"> data-active </c:if>
   71.39 +        <c:if test="${not empty entry.title}">title="<fmt:message key="${entry.title}"/>" </c:if>
   71.40 +    >
   71.41 +        <c:if test="${not empty entry.iconColor}">
   71.42 +            <c:if test="${entry.iconUseCssClass}">
   71.43 +                <div class="navmenu-icon ${entry.iconColor}"></div>
   71.44 +            </c:if>
   71.45 +            <c:if test="${not entry.iconUseCssClass}">
   71.46 +                <div class="navmenu-icon" style="background: ${entry.iconColor}"></div>
   71.47 +            </c:if>
   71.48 +        </c:if>
   71.49 +        <a href="./${entry.href}">
   71.50 +            <c:if test="${entry.resolveCaption}">
   71.51 +                <fmt:message key="${entry.caption}"/>
   71.52 +            </c:if>
   71.53 +            <c:if test="${not entry.resolveCaption}">
   71.54 +                <c:out value="${entry.caption}"/>
   71.55 +            </c:if>
   71.56 +        </a>
   71.57 +    </div>
   71.58 +</c:forEach>
    72.1 --- a/src/main/webapp/WEB-INF/jspf/project-header.jspf	Sat Jan 23 14:47:59 2021 +0100
    72.2 +++ b/src/main/webapp/WEB-INF/jspf/project-header.jspf	Fri Apr 02 11:59:14 2021 +0200
    72.3 @@ -22,7 +22,7 @@
    72.4              </c:if>
    72.5          </div>
    72.6      </div>
    72.7 -    <c:if test="${not empty component and component.id gt 0}">
    72.8 +    <c:if test="${not empty component}">
    72.9          <div class="row">
   72.10              <div class="caption"><fmt:message key="component"/>:</div>
   72.11              <div><c:out value="${component.name}"/></div>

mercurial