# HG changeset patch # User Mike Becker # Date 1617357554 -7200 # Node ID e8eecee6aadf664f8051b2e49ae0de46a9462554 # Parent 61669abf277f8f8710e5f81ec5b6fdb6082c70f0 completes kotlin migration diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/AbstractServlet.java --- a/src/main/java/de/uapcore/lightpit/AbstractServlet.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,460 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -package de.uapcore.lightpit; - -import de.uapcore.lightpit.dao.DataAccessObject; -import de.uapcore.lightpit.dao.PostgresDataAccessObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.io.IOException; -import java.lang.reflect.*; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * A special implementation of a HTTPServlet which is focused on implementing - * the necessary functionality for LightPIT pages. - */ -public abstract class AbstractServlet extends HttpServlet { - - private static final Logger LOG = LoggerFactory.getLogger(AbstractServlet.class); - - /** - * Invocation mapping gathered from the {@link RequestMapping} annotations. - *

- * Paths in this map must always start with a leading slash, although - * the specification in the annotation must not start with a leading slash. - *

- * The reason for this is the different handling of empty paths in - * {@link HttpServletRequest#getPathInfo()}. - */ - private final Map> mappings = new HashMap<>(); - - /** - * Creates a set of data access objects for the specified connection. - * - * @param connection the SQL connection - * @return a set of data access objects - */ - private DataAccessObject createDataAccessObjects(Connection connection) { - final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); - if (df.getDialect() == DataSourceProvider.Dialect.Postgres) { - return new PostgresDataAccessObject(connection); - } - throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug."); - } - - private void invokeMapping(Map.Entry mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException { - final var pathPattern = mapping.getKey(); - final var method = mapping.getValue(); - try { - LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName()); - final var paramTypes = method.getParameterTypes(); - final var paramValues = new Object[paramTypes.length]; - for (int i = 0; i < paramTypes.length; i++) { - if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) { - paramValues[i] = req; - } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) { - paramValues[i] = resp; - } - if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) { - paramValues[i] = dao; - } - if (paramTypes[i].isAssignableFrom(PathParameters.class)) { - paramValues[i] = pathPattern.obtainPathParameters(sanitizeRequestPath(req)); - } - } - method.invoke(this, paramValues); - } catch (InvocationTargetException ex) { - LOG.error("invocation of method {}::{} failed: {}", - method.getDeclaringClass().getName(), method.getName(), ex.getTargetException().getMessage()); - LOG.debug("Details: ", ex.getTargetException()); - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getTargetException().getMessage()); - } catch (ReflectiveOperationException | ClassCastException ex) { - LOG.error("invocation of method {}::{} failed: {}", - method.getDeclaringClass().getName(), method.getName(), ex.getMessage()); - LOG.debug("Details: ", ex); - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); - } - } - - @Override - public void init() throws ServletException { - scanForRequestMappings(); - - LOG.trace("{} initialized", getServletName()); - } - - private void scanForRequestMappings() { - try { - Method[] methods = getClass().getDeclaredMethods(); - for (Method method : methods) { - Optional mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class)); - if (mapping.isPresent()) { - if (mapping.get().requestPath().isBlank()) { - LOG.warn("{} is annotated with {} but request path is empty", - method.getName(), RequestMapping.class.getSimpleName() - ); - continue; - } - - if (!Modifier.isPublic(method.getModifiers())) { - LOG.warn("{} is annotated with {} but is not public", - method.getName(), RequestMapping.class.getSimpleName() - ); - continue; - } - if (Modifier.isAbstract(method.getModifiers())) { - LOG.warn("{} is annotated with {} but is abstract", - method.getName(), RequestMapping.class.getSimpleName() - ); - continue; - } - - boolean paramsInjectible = true; - for (var param : method.getParameterTypes()) { - paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param) - || HttpServletResponse.class.isAssignableFrom(param) - || PathParameters.class.isAssignableFrom(param) - || DataAccessObject.class.isAssignableFrom(param); - } - if (paramsInjectible) { - try { - PathPattern pathPattern = new PathPattern(mapping.get().requestPath()); - - final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>()); - final var currentMapping = methodMappings.putIfAbsent(pathPattern, method); - if (currentMapping != null) { - LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}", - mapping.get().method(), - mapping.get().requestPath(), - method.getName(), - getClass().getSimpleName(), - currentMapping.getName() - ); - } - - LOG.debug("{} {} maps to {}::{}", - mapping.get().method(), - mapping.get().requestPath(), - getClass().getSimpleName(), - method.getName() - ); - } catch (IllegalArgumentException ex) { - LOG.warn("Request mapping for {} failed: path pattern '{}' is syntactically invalid", - method.getName(), mapping.get().requestPath() - ); - } - } else { - LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest, HttpServletResponse, PathParameters, and DataAccessObjects are allowed", - method.getName(), RequestMapping.class.getSimpleName() - ); - } - } - } - } catch (SecurityException ex) { - LOG.error("Scan for request mappings on declared methods failed.", ex); - } - } - - @Override - public void destroy() { - mappings.clear(); - LOG.trace("{} destroyed", getServletName()); - } - - /** - * Sets the name of the content page. - *

- * It is sufficient to specify the name without any extension. The extension - * is added automatically if not specified. - * - * @param req the servlet request object - * @param pageName the name of the content page - * @see Constants#REQ_ATTR_CONTENT_PAGE - */ - protected void setContentPage(HttpServletRequest req, String pageName) { - req.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(pageName)); - } - - /** - * Sets the navigation menu. - * - * @param req the servlet request object - * @param jspName the name of the menu's jsp file - * @see Constants#REQ_ATTR_NAVIGATION - */ - protected void setNavigationMenu(HttpServletRequest req, String jspName) { - req.setAttribute(Constants.REQ_ATTR_NAVIGATION, jspPath(jspName)); - } - - /** - * @param req the servlet request object - * @param location the location where to redirect - * @see Constants#REQ_ATTR_REDIRECT_LOCATION - */ - protected void setRedirectLocation(HttpServletRequest req, String location) { - if (location.startsWith("./")) { - location = location.replaceFirst("\\./", baseHref(req)); - } - req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location); - } - - /** - * Specifies the names of additional stylesheets used by this Servlet. - *

- * It is sufficient to specify the name without any extension. The extension - * is added automatically if not specified. - * - * @param req the servlet request object - * @param stylesheets the names of the stylesheets - */ - public void setStylesheet(HttpServletRequest req, String ... stylesheets) { - req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Arrays - .stream(stylesheets) - .map(s -> enforceExt(s, ".css")) - .collect(Collectors.toUnmodifiableList())); - } - - /** - * Sets the view model object. - * The type must match the expected type in the JSP file. - * - * @param req the servlet request object - * @param viewModel the view model object - */ - public void setViewModel(HttpServletRequest req, Object viewModel) { - req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel); - } - - private Optional parseParameter(String paramValue, Class clazz) { - if (paramValue == null) return Optional.empty(); - if (clazz.equals(Boolean.class)) { - if (paramValue.equalsIgnoreCase("false") || paramValue.equals("0")) { - return Optional.of((T) Boolean.FALSE); - } else { - return Optional.of((T) Boolean.TRUE); - } - } - if (clazz.equals(String.class)) return Optional.of((T) paramValue); - if (java.sql.Date.class.isAssignableFrom(clazz)) { - try { - return Optional.of((T) java.sql.Date.valueOf(paramValue)); - } catch (IllegalArgumentException ex) { - return Optional.empty(); - } - } - try { - final Constructor ctor = clazz.getConstructor(String.class); - return Optional.of(ctor.newInstance(paramValue)); - } catch (ReflectiveOperationException e) { - // does not type check and is not convertible - treat as if the parameter was never set - return Optional.empty(); - } - } - - /** - * Obtains a request parameter of the specified type. - * The specified type must have a single-argument constructor accepting a string to perform conversion. - * The constructor of the specified type may throw an exception on conversion failures. - * - * @param req the servlet request object - * @param clazz the class object of the expected type - * @param name the name of the parameter - * @param the expected type - * @return the parameter value or an empty optional, if no parameter with the specified name was found - */ - protected Optional getParameter(HttpServletRequest req, Class clazz, String name) { - if (clazz.isArray()) { - final String[] paramValues = req.getParameterValues(name); - int len = paramValues == null ? 0 : paramValues.length; - final var array = (T) Array.newInstance(clazz.getComponentType(), len); - for (int i = 0; i < len; i++) { - try { - final Constructor ctor = clazz.getComponentType().getConstructor(String.class); - Array.set(array, i, ctor.newInstance(paramValues[i])); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - return Optional.of(array); - } else { - return parseParameter(req.getParameter(name), clazz); - } - } - - /** - * Tries to look up an entity with a key obtained from a request parameter. - * - * @param req the servlet request object - * @param clazz the class representing the type of the request parameter - * @param name the name of the request parameter - * @param find the find function (typically a DAO function) - * @param the type of the request parameter - * @param the type of the looked up entity - * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing - * @throws SQLException if the find function throws an exception - */ - protected Optional findByParameter(HttpServletRequest req, Class clazz, String name, Function find) { - final var param = getParameter(req, clazz, name); - if (param.isPresent()) { - return Optional.ofNullable(find.apply(param.get())); - } else { - return Optional.empty(); - } - } - - protected void setAttributeFromParameter(HttpServletRequest req, String name) { - final var parm = req.getParameter(name); - if (parm != null) { - req.setAttribute(name, parm); - } - } - - private String sanitizeRequestPath(HttpServletRequest req) { - return Optional.ofNullable(req.getPathInfo()).orElse("/"); - } - - private Optional> findMapping(HttpMethod method, HttpServletRequest req) { - return Optional.ofNullable(mappings.get(method)).flatMap(rm -> - rm.entrySet().stream().filter( - kv -> kv.getKey().matches(sanitizeRequestPath(req)) - ).findAny() - ); - } - - protected void renderSite(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - req.getRequestDispatcher(jspPath("site")).forward(req, resp); - } - - protected Optional availableLanguages() { - return Optional.ofNullable(getServletContext().getInitParameter(Constants.CTX_ATTR_LANGUAGES)).map((x) -> x.split("\\s*,\\s*")); - } - - private static String baseHref(HttpServletRequest req) { - return String.format("%s://%s:%d%s/", - req.getScheme(), - req.getServerName(), - req.getServerPort(), - req.getContextPath()); - } - - private static String enforceExt(String filename, String ext) { - return filename.endsWith(ext) ? filename : filename + ext; - } - - private static String jspPath(String filename) { - return enforceExt(Constants.JSP_PATH_PREFIX + filename, ".jsp"); - } - - private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - // the very first thing to do is to force UTF-8 - req.setCharacterEncoding("UTF-8"); - - // choose the requested language as session language (if available) or fall back to english, otherwise - HttpSession session = req.getSession(); - if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { - Optional> availableLanguages = availableLanguages().map(Arrays::asList); - Optional reqLocale = Optional.of(req.getLocale()); - Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH); - session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale); - LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage()); - } else { - Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE); - resp.setLocale(sessionLocale); - LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale); - } - - // set some internal request attributes - final String fullPath = req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse(""); - req.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref(req)); - req.setAttribute(Constants.REQ_ATTR_PATH, fullPath); - - // if this is an error path, bypass the normal flow - if (fullPath.startsWith("/error/")) { - final var mapping = findMapping(method, req); - if (mapping.isPresent()) { - invokeMapping(mapping.get(), req, resp, null); - } - return; - } - - // obtain a connection and create the data access objects - final var db = (DataSourceProvider) req.getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME()); - final var ds = db.getDataSource(); - if (ds == null) { - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details."); - return; - } - try (final var connection = ds.getConnection()) { - final var dao = createDataAccessObjects(connection); - try { - connection.setAutoCommit(false); - // call the handler, if available, or send an HTTP 404 error - final var mapping = findMapping(method, req); - if (mapping.isPresent()) { - invokeMapping(mapping.get(), req, resp, dao); - } else { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - } - connection.commit(); - } catch (SQLException ex) { - LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage()); - LOG.debug("Details: ", ex); - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode()); - connection.rollback(); - } - } catch (SQLException ex) { - LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage()); - LOG.debug("Details: ", ex); - resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode()); - } - } - - @Override - protected final void doGet(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - doProcess(HttpMethod.GET, req, resp); - } - - @Override - protected final void doPost(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - doProcess(HttpMethod.POST, req, resp); - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/modules/ErrorModule.java --- a/src/main/java/de/uapcore/lightpit/modules/ErrorModule.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -package de.uapcore.lightpit.modules; - -import de.uapcore.lightpit.AbstractServlet; -import de.uapcore.lightpit.HttpMethod; -import de.uapcore.lightpit.RequestMapping; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Optional; - -@WebServlet( - name = "ErrorModule", - urlPatterns = "/error/*" -) -public final class ErrorModule extends AbstractServlet { - - public static final String REQ_ATTR_RETURN_LINK = "returnLink"; - - @RequestMapping(requestPath = "generic", method = HttpMethod.GET) - public void onError(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Optional.ofNullable(req.getHeader("Referer")).ifPresent( - referer -> req.setAttribute(REQ_ATTR_RETURN_LINK, referer) - ); - - setStylesheet(req, "error"); - setContentPage(req, "error"); - - renderSite(req, resp); - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/modules/LanguageModule.java --- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -package de.uapcore.lightpit.modules; - -import de.uapcore.lightpit.AbstractServlet; -import de.uapcore.lightpit.Constants; -import de.uapcore.lightpit.HttpMethod; -import de.uapcore.lightpit.RequestMapping; -import de.uapcore.lightpit.viewmodel.LanguageView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.*; - -@WebServlet( - name = "LanguageModule", - urlPatterns = "/language/*" -) -public final class LanguageModule extends AbstractServlet { - - private static final Logger LOG = LoggerFactory.getLogger(LanguageModule.class); - - private final List languages = new ArrayList<>(); - - @Override - public void init() throws ServletException { - super.init(); - - Optional langs = availableLanguages(); - if (langs.isPresent()) { - for (String lang : langs.get()) { - try { - Locale locale = Locale.forLanguageTag(lang); - if (locale.getLanguage().isEmpty()) { - throw new IllformedLocaleException(); - } - languages.add(locale); - } catch (IllformedLocaleException ex) { - LOG.warn("Specified language {} in context parameter cannot be mapped to an existing locale - skipping.", lang); - } - } - - } else { - languages.add(Locale.ENGLISH); - LOG.warn("Context parameter 'available-languages' not found. Only english will be available."); - } - } - - @Override - public void destroy() { - super.destroy(); - languages.clear(); - } - - @RequestMapping(method = HttpMethod.GET) - public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - final var viewModel = new LanguageView(); - viewModel.setLanguages(languages); - viewModel.setBrowserLanguage(req.getLocale()); - viewModel.setCurrentLanguage((Locale)req.getSession().getAttribute(Constants.SESSION_ATTR_LANGUAGE)); - - setViewModel(req, viewModel); - setStylesheet(req, "language"); - setContentPage(req, "language"); - - renderSite(req, resp); - } - - @RequestMapping(method = HttpMethod.POST) - public void switchLanguage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - Optional chosenLanguage = Optional.ofNullable(req.getParameter("language")) - .map(Locale::forLanguageTag) - .filter((l) -> !l.getLanguage().isEmpty()); - - chosenLanguage.ifPresent((l) -> req.getSession().setAttribute(Constants.SESSION_ATTR_LANGUAGE, l)); - chosenLanguage.ifPresent(resp::setLocale); - - handle(req, resp); - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,660 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -package de.uapcore.lightpit.modules; - - -import de.uapcore.lightpit.*; -import de.uapcore.lightpit.dao.DataAccessObject; -import de.uapcore.lightpit.entities.*; -import de.uapcore.lightpit.filter.AllFilter; -import de.uapcore.lightpit.filter.IssueFilter; -import de.uapcore.lightpit.filter.NoneFilter; -import de.uapcore.lightpit.filter.SpecificFilter; -import de.uapcore.lightpit.types.IssueCategory; -import de.uapcore.lightpit.types.IssueStatus; -import de.uapcore.lightpit.types.VersionStatus; -import de.uapcore.lightpit.types.WebColor; -import de.uapcore.lightpit.viewmodel.*; -import de.uapcore.lightpit.viewmodel.util.IssueSorter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.sql.Date; -import java.sql.SQLException; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@WebServlet( - name = "ProjectsModule", - urlPatterns = "/projects/*" -) -public final class ProjectsModule extends AbstractServlet { - - private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class); - - private static int parseIntOrZero(String str) { - try { - return Integer.parseInt(str); - } catch (NumberFormatException ex) { - return 0; - } - } - - private void populate(ProjectView viewModel, PathParameters pathParameters, DataAccessObject dao) { - dao.listProjects().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add); - - if (pathParameters == null) - return; - - // Select Project - final var project = dao.findProjectByNode(pathParameters.get("project")); - if (project == null) - return; - - final var info = new ProjectInfo(project); - info.setVersions(dao.listVersions(project)); - info.setComponents(dao.listComponents(project)); - info.setIssueSummary(dao.collectIssueSummary(project)); - viewModel.setProjectInfo(info); - - // Select Version - final var versionNode = pathParameters.get("version"); - if (versionNode != null) { - if ("no-version".equals(versionNode)) { - viewModel.setVersionFilter(ProjectView.NO_VERSION); - } else if ("all-versions".equals(versionNode)) { - viewModel.setVersionFilter(ProjectView.ALL_VERSIONS); - } else { - viewModel.setVersionFilter(dao.findVersionByNode(project, versionNode)); - } - } - - // Select Component - final var componentNode = pathParameters.get("component"); - if (componentNode != null) { - if ("no-component".equals(componentNode)) { - viewModel.setComponentFilter(ProjectView.NO_COMPONENT); - } else if ("all-components".equals(componentNode)) { - viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS); - } else { - viewModel.setComponentFilter(dao.findComponentByNode(project, componentNode)); - } - } - } - - private static String sanitizeNode(String node, String defaultValue) { - String result = node == null || node.isBlank() ? defaultValue : node; - result = result.replace('/', '-'); - if (result.equals(".") || result.equals("..")) { - return "_"+result; - } else { - return result; - } - } - - private void forwardView(HttpServletRequest req, HttpServletResponse resp, ProjectView viewModel, String name) throws ServletException, IOException { - setViewModel(req, viewModel); - setContentPage(req, name); - setStylesheet(req, "projects"); - setNavigationMenu(req, "project-navmenu"); - renderSite(req, resp); - } - - @RequestMapping(method = HttpMethod.GET) - public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException { - final var viewModel = new ProjectView(); - populate(viewModel, null, dao); - - for (var info : viewModel.getProjectList()) { - info.setVersions(dao.listVersions(info.getProject())); - info.setIssueSummary(dao.collectIssueSummary(info.getProject())); - } - - forwardView(req, resp, viewModel, "projects"); - } - - private void configureProjectEditor(ProjectEditView viewModel, Project project, DataAccessObject dao) { - viewModel.setProject(project); - viewModel.setUsers(dao.listUsers()); - } - - @RequestMapping(requestPath = "$project/edit", method = HttpMethod.GET) - public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws IOException, SQLException, ServletException { - final var viewModel = new ProjectEditView(); - populate(viewModel, pathParams, dao); - - if (!viewModel.isProjectInfoPresent()) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - configureProjectEditor(viewModel, viewModel.getProjectInfo().getProject(), dao); - forwardView(req, resp, viewModel, "project-form"); - } - - @RequestMapping(requestPath = "create", method = HttpMethod.GET) - public void create(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException { - final var viewModel = new ProjectEditView(); - populate(viewModel, null, dao); - configureProjectEditor(viewModel, new Project(-1), dao); - forwardView(req, resp, viewModel, "project-form"); - } - - @RequestMapping(requestPath = "commit", method = HttpMethod.POST) - public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { - - try { - final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow()); - project.setName(getParameter(req, String.class, "name").orElseThrow()); - - final var node = getParameter(req, String.class, "node").orElse(null); - project.setNode(sanitizeNode(node, project.getName())); - getParameter(req, Integer.class, "ordinal").ifPresent(project::setOrdinal); - - getParameter(req, String.class, "description").ifPresent(project::setDescription); - getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl); - getParameter(req, Integer.class, "owner").map( - ownerId -> ownerId >= 0 ? new User(ownerId) : null - ).ifPresent(project::setOwner); - - if (project.getId() > 0) { - dao.updateProject(project); - } else { - dao.insertProject(project); - } - - setRedirectLocation(req, "./projects/"); - setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - LOG.debug("Successfully updated project {}", project.getName()); - - renderSite(req, resp); - } catch (NoSuchElementException | IllegalArgumentException ex) { - resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - // TODO: implement - fix issue #21 - } - } - - @RequestMapping(requestPath = "$project/$component/$version/issues/", method = HttpMethod.GET) - public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws SQLException, IOException, ServletException { - final var viewModel = new ProjectDetailsView(); - populate(viewModel, pathParams, dao); - - if (!viewModel.isEveryFilterValid()) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - final var project = viewModel.getProjectInfo().getProject(); - final var version = viewModel.getVersionFilter(); - final var component = viewModel.getComponentFilter(); - - // TODO: use new IssueFilter class for the ViewModel - - final var projectFilter = new SpecificFilter<>(project); - final IssueFilter filter; - if (version.equals(ProjectView.NO_VERSION)) { - if (component.equals(ProjectView.ALL_COMPONENTS)) { - filter = new IssueFilter(projectFilter, - new NoneFilter<>(), - new AllFilter<>() - ); - } else if (component.equals(ProjectView.NO_COMPONENT)) { - filter = new IssueFilter(projectFilter, - new NoneFilter<>(), - new NoneFilter<>() - ); - } else { - filter = new IssueFilter(projectFilter, - new NoneFilter<>(), - new SpecificFilter<>(component) - ); - } - } else if (version.equals(ProjectView.ALL_VERSIONS)) { - if (component.equals(ProjectView.ALL_COMPONENTS)) { - filter = new IssueFilter(projectFilter, - new AllFilter<>(), - new AllFilter<>() - ); - } else if (component.equals(ProjectView.NO_COMPONENT)) { - filter = new IssueFilter(projectFilter, - new AllFilter<>(), - new NoneFilter<>() - ); - } else { - filter = new IssueFilter(projectFilter, - new AllFilter<>(), - new SpecificFilter<>(component) - ); - } - } else { - if (component.equals(ProjectView.ALL_COMPONENTS)) { - filter = new IssueFilter(projectFilter, - new SpecificFilter<>(version), - new AllFilter<>() - ); - } else if (component.equals(ProjectView.NO_COMPONENT)) { - filter = new IssueFilter(projectFilter, - new SpecificFilter<>(version), - new NoneFilter<>() - ); - } else { - filter = new IssueFilter(projectFilter, - new SpecificFilter<>(version), - new SpecificFilter<>(component) - ); - } - } - - final var issues = dao.listIssues(filter); - issues.sort(new IssueSorter( - new IssueSorter.Criteria(IssueSorter.Field.DONE, true), - new IssueSorter.Criteria(IssueSorter.Field.ETA, true), - new IssueSorter.Criteria(IssueSorter.Field.UPDATED, false) - )); - - - viewModel.getProjectDetails().updateDetails(issues); - if (version.getId() > 0) - viewModel.getProjectDetails().updateVersionInfo(version); - - forwardView(req, resp, viewModel, "project-details"); - } - - @RequestMapping(requestPath = "$project/versions/", method = HttpMethod.GET) - public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException { - final var viewModel = new VersionsView(); - populate(viewModel, pathParameters, dao); - - final var projectInfo = viewModel.getProjectInfo(); - if (projectInfo == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - final var issues = dao.listIssues( - new IssueFilter( - new SpecificFilter<>(projectInfo.getProject()), - new AllFilter<>(), - new AllFilter<>() - ) - ); - viewModel.update(projectInfo.getVersions(), issues); - - forwardView(req, resp, viewModel, "versions"); - } - - @RequestMapping(requestPath = "$project/versions/$version/edit", method = HttpMethod.GET) - public void editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { - final var viewModel = new VersionEditView(); - populate(viewModel, pathParameters, dao); - - if (viewModel.getProjectInfo() == null || viewModel.getVersionFilter() == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - viewModel.setVersion(viewModel.getVersionFilter()); - - forwardView(req, resp, viewModel, "version-form"); - } - - @RequestMapping(requestPath = "$project/create-version", method = HttpMethod.GET) - public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { - final var viewModel = new VersionEditView(); - populate(viewModel, pathParameters, dao); - - if (viewModel.getProjectInfo() == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - viewModel.setVersion(new Version(-1, viewModel.getProjectInfo().getProject().getId())); - - forwardView(req, resp, viewModel, "version-form"); - } - - @RequestMapping(requestPath = "commit-version", method = HttpMethod.POST) - public void commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { - - try { - final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow()); - if (project == null) { - // TODO: improve error handling, because not found is not correct for this POST request - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - final var version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), project.getId()); - version.setName(getParameter(req, String.class, "name").orElseThrow()); - - final var node = getParameter(req, String.class, "node").orElse(null); - version.setNode(sanitizeNode(node, version.getName())); - - getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal); - version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow())); - - if (version.getId() > 0) { - dao.updateVersion(version); - } else { - dao.insertVersion(version); - } - - setRedirectLocation(req, "./projects/" + project.getNode() + "/versions/"); - setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - - renderSite(req, resp); - } catch (NoSuchElementException | IllegalArgumentException ex) { - resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - // TODO: implement - fix issue #21 - } - } - - @RequestMapping(requestPath = "$project/components/", method = HttpMethod.GET) - public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException { - final var viewModel = new ComponentsView(); - populate(viewModel, pathParameters, dao); - - final var projectInfo = viewModel.getProjectInfo(); - if (projectInfo == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - final var issues = dao.listIssues( - new IssueFilter( - new SpecificFilter<>(projectInfo.getProject()), - new AllFilter<>(), - new AllFilter<>() - ) - ); - viewModel.update(projectInfo.getComponents(), issues); - - forwardView(req, resp, viewModel, "components"); - } - - @RequestMapping(requestPath = "$project/components/$component/edit", method = HttpMethod.GET) - public void editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { - final var viewModel = new ComponentEditView(); - populate(viewModel, pathParameters, dao); - - if (viewModel.getProjectInfo() == null || viewModel.getComponentFilter() == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - viewModel.setComponent(viewModel.getComponentFilter()); - viewModel.setUsers(dao.listUsers()); - - forwardView(req, resp, viewModel, "component-form"); - } - - @RequestMapping(requestPath = "$project/create-component", method = HttpMethod.GET) - public void createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { - final var viewModel = new ComponentEditView(); - populate(viewModel, pathParameters, dao); - - if (viewModel.getProjectInfo() == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - viewModel.setComponent(new Component(-1, viewModel.getProjectInfo().getProject().getId())); - viewModel.setUsers(dao.listUsers()); - - forwardView(req, resp, viewModel, "component-form"); - } - - @RequestMapping(requestPath = "commit-component", method = HttpMethod.POST) - public void commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { - - try { - final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow()); - if (project == null) { - // TODO: improve error handling, because not found is not correct for this POST request - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - final var component = new Component(getParameter(req, Integer.class, "id").orElseThrow(), project.getId()); - component.setName(getParameter(req, String.class, "name").orElseThrow()); - - final var node = getParameter(req, String.class, "node").orElse(null); - component.setNode(sanitizeNode(node, component.getName())); - - component.setColor(getParameter(req, WebColor.class, "color").orElseThrow()); - getParameter(req, Integer.class, "ordinal").ifPresent(component::setOrdinal); - getParameter(req, Integer.class, "lead").map( - userid -> userid >= 0 ? new User(userid) : null - ).ifPresent(component::setLead); - getParameter(req, String.class, "description").ifPresent(component::setDescription); - - if (component.getId() > 0) { - dao.updateComponent(component); - } else { - dao.insertComponent(component); - } - - setRedirectLocation(req, "./projects/" + project.getNode() + "/components/"); - setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - - renderSite(req, resp); - } catch (NoSuchElementException | IllegalArgumentException ex) { - resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - // TODO: implement - fix issue #21 - } - } - - private void configureIssueEditor(IssueEditView viewModel, Issue issue, DataAccessObject dao) { - final var project = viewModel.getProjectInfo().getProject(); - issue.setProject(project); // automatically set current project for new issues - viewModel.setIssue(issue); - viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions()); - viewModel.setUsers(dao.listUsers()); - viewModel.setComponents(dao.listComponents(project)); - } - - @RequestMapping(requestPath = "$project/issues/$issue/view", method = HttpMethod.GET) - public void viewIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { - final var viewModel = new IssueDetailView(); - populate(viewModel, pathParameters, dao); - - final var projectInfo = viewModel.getProjectInfo(); - if (projectInfo == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue"))); - if (issue == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - viewModel.setIssue(issue); - viewModel.setComments(dao.listComments(issue)); - - viewModel.processMarkdown(); - - forwardView(req, resp, viewModel, "issue-view"); - } - - // TODO: why should the issue editor be child of $project? - @RequestMapping(requestPath = "$project/issues/$issue/edit", method = HttpMethod.GET) - public void editIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException { - final var viewModel = new IssueEditView(); - populate(viewModel, pathParameters, dao); - - final var projectInfo = viewModel.getProjectInfo(); - if (projectInfo == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue"))); - if (issue == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - configureIssueEditor(viewModel, issue, dao); - - forwardView(req, resp, viewModel, "issue-form"); - } - - @RequestMapping(requestPath = "$project/create-issue", method = HttpMethod.GET) - public void createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { - final var viewModel = new IssueEditView(); - populate(viewModel, pathParameters, dao); - - final var projectInfo = viewModel.getProjectInfo(); - if (projectInfo == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - setAttributeFromParameter(req, "more"); - setAttributeFromParameter(req, "cid"); - setAttributeFromParameter(req, "vid"); - - final var issue = new Issue(-1, projectInfo.getProject(), null); - issue.setProject(projectInfo.getProject()); - configureIssueEditor(viewModel, issue, dao); - - forwardView(req, resp, viewModel, "issue-form"); - } - - @RequestMapping(requestPath = "commit-issue", method = HttpMethod.POST) - public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { - try { - final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow()); - if (project == null) { - // TODO: improve error handling, because not found is not correct for this POST request - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - final var componentId = getParameter(req, Integer.class, "component"); - final Component component; - if (componentId.isPresent()) { - component = dao.findComponent(componentId.get()); - } else { - component = null; - } - final var issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), project, component); - getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory); - getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus); - issue.setSubject(getParameter(req, String.class, "subject").orElseThrow()); - getParameter(req, Integer.class, "assignee").map(userid -> { - if (userid >= 0) { - return new User(userid); - } else if (userid == -2) { - return Optional.ofNullable(component).map(Component::getLead).orElse(null); - } else { - return null; - } - } - ).ifPresent(issue::setAssignee); - getParameter(req, String.class, "description").ifPresent(issue::setDescription); - getParameter(req, Date.class, "eta").ifPresent(issue::setEta); - - getParameter(req, Integer[].class, "affected") - .map(Stream::of) - .map(stream -> - stream.map(id -> new Version(id, project.getId())) - .collect(Collectors.toList()) - ).ifPresent(issue::setAffectedVersions); - getParameter(req, Integer[].class, "resolved") - .map(Stream::of) - .map(stream -> - stream.map(id -> new Version(id, project.getId())) - .collect(Collectors.toList()) - ).ifPresent(issue::setResolvedVersions); - - if (issue.getId() > 0) { - dao.updateIssue(issue); - } else { - dao.insertIssue(issue); - } - - if (getParameter(req, Boolean.class, "create-another").orElse(false)) { - // TODO: fix #38 - automatically select component (and version) - setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/create-issue?more=true"); - } else{ - setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/issues/" + issue.getId() + "/view"); - } - setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - - renderSite(req, resp); - } catch (NoSuchElementException | IllegalArgumentException ex) { - resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - // TODO: implement - fix issue #21 - } - } - - @RequestMapping(requestPath = "commit-issue-comment", method = HttpMethod.POST) - public void commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { - final var issueIdParam = getParameter(req, Integer.class, "issueid"); - if (issueIdParam.isEmpty()) { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form."); - return; - } - final var issue = dao.findIssue(issueIdParam.get()); - if (issue == null) { - resp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - try { - final var issueComment = new IssueComment(getParameter(req, Integer.class, "commentid").orElse(-1), issue.getId()); - issueComment.setComment(getParameter(req, String.class, "comment").orElse("")); - - if (issueComment.getComment().isBlank()) { - throw new IllegalArgumentException("comment.null"); - } - - LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId()); - if (req.getRemoteUser() != null) { - Optional.ofNullable(dao.findUserByName(req.getRemoteUser())).ifPresent(issueComment::setAuthor); - } - - dao.insertComment(issueComment); - - setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/view"); - setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - - renderSite(req, resp); - } catch (NoSuchElementException | IllegalArgumentException ex) { - resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); - // TODO: implement - fix issue #21 - } - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/modules/UsersModule.java --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -package de.uapcore.lightpit.modules; - -import de.uapcore.lightpit.AbstractServlet; -import de.uapcore.lightpit.Constants; -import de.uapcore.lightpit.HttpMethod; -import de.uapcore.lightpit.RequestMapping; -import de.uapcore.lightpit.dao.DataAccessObject; -import de.uapcore.lightpit.entities.User; -import de.uapcore.lightpit.viewmodel.UsersEditView; -import de.uapcore.lightpit.viewmodel.UsersView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.sql.SQLException; -import java.util.NoSuchElementException; - -@WebServlet( - name = "UsersModule", - urlPatterns = "/teams/*" -) -public final class UsersModule extends AbstractServlet { - - private static final Logger LOG = LoggerFactory.getLogger(UsersModule.class); - - @RequestMapping(method = HttpMethod.GET) - public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException { - final var viewModel = new UsersView(); - viewModel.setUsers(dao.listUsers()); - setViewModel(req, viewModel); - setContentPage(req, "users"); - - renderSite(req, resp); - } - - @RequestMapping(requestPath = "edit", method = HttpMethod.GET) - public void edit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException { - - final var viewModel = new UsersEditView(); - viewModel.setUser(findByParameter(req, Integer.class, "id", dao::findUser).orElse(new User(-1))); - - setViewModel(req, viewModel); - setContentPage(req, "user-form"); - - renderSite(req, resp); - } - - @RequestMapping(requestPath = "commit", method = HttpMethod.POST) - public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException { - - User user = new User(-1); - try { - user = new User(getParameter(req, Integer.class, "userid").orElseThrow()); - user.setUsername(getParameter(req, String.class, "username").orElseThrow()); - getParameter(req, String.class, "givenname").ifPresent(user::setGivenname); - getParameter(req, String.class, "lastname").ifPresent(user::setLastname); - getParameter(req, String.class, "mail").ifPresent(user::setMail); - - if (user.getId() > 0) { - dao.updateUser(user); - } else { - dao.insertUser(user); - } - - setRedirectLocation(req, "./teams/"); - setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - - LOG.debug("Successfully updated user {}", user.getUsername()); - } catch (NoSuchElementException | IllegalArgumentException ex) { - final var viewModel = new UsersEditView(); - viewModel.setUser(user); - // TODO: viewModel.setErrorText() - setViewModel(req, viewModel); - setContentPage(req, "user-form"); - LOG.warn("Form validation failure: {}", ex.getMessage()); - LOG.debug("Details:", ex); - } - - renderSite(req, resp); - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ComponentEditView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ComponentEditView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Component; -import de.uapcore.lightpit.entities.User; - -import java.util.List; - -public class ComponentEditView extends ProjectView { - private Component component; - private List users; - private String errorText; - - public ComponentEditView() { - setSelectedPage(SELECTED_PAGE_COMPONENTS); - } - - public void setComponent(Component component) { - this.component = component; - } - - public Component getComponent() { - return component; - } - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } - - public String getErrorText() { - return errorText; - } - - public void setErrorText(String errorText) { - this.errorText = errorText; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ComponentInfo.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ComponentInfo.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Component; -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.entities.IssueSummary; - -import java.util.ArrayList; -import java.util.List; - -public class ComponentInfo { - - private final Component component; - - private final IssueSummary issueSummary = new IssueSummary(); - - private final List issues = new ArrayList<>(); - - public ComponentInfo(Component component) { - this.component = component; - } - - public Component getComponent() { - return component; - } - - public IssueSummary getIssueSummary() { - return issueSummary; - } - - public List getIssues() { - return issues; - } - - public void collectIssues(List issues) { - for (Issue issue : issues) { - if (component.equals(issue.getComponent())) { - this.issues.add(issue); - this.issueSummary.add(issue); - } - } - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ComponentsView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ComponentsView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Component; -import de.uapcore.lightpit.entities.Issue; - -import java.util.ArrayList; -import java.util.List; - -public class ComponentsView extends ProjectView { - - private List componentInfos = new ArrayList<>(); - - public ComponentsView() { - setSelectedPage(SELECTED_PAGE_COMPONENTS); - } - - public void update(List components, List issues) { - componentInfos.clear(); - for (var component : components) { - final var info = new ComponentInfo(component); - info.collectIssues(issues); - componentInfos.add(info); - } - } - - public List getComponentInfos() { - return componentInfos; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/IssueDetailView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueDetailView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; -import com.vladsch.flexmark.ext.tables.TablesExtension; -import com.vladsch.flexmark.html.HtmlRenderer; -import com.vladsch.flexmark.parser.Parser; -import com.vladsch.flexmark.util.data.MutableDataSet; -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.entities.IssueComment; - -import java.util.Arrays; -import java.util.List; - -public class IssueDetailView extends ProjectView { - private Issue issue; - - private List comments; - - public void setIssue(Issue issue) { - this.issue = issue; - } - - public Issue getIssue() { - return issue; - } - - public List getComments() { - return comments; - } - - public void setComments(List comments) { - this.comments = comments; - } - - public void processMarkdown() { - final var options = new MutableDataSet() - .set(Parser.EXTENSIONS, Arrays.asList(TablesExtension.create(), StrikethroughExtension.create())) - .toImmutable(); - final var parser = Parser.builder(options).build(); - final var renderer = HtmlRenderer.builder(options).build(); - - issue.setDescription(renderer.render(parser.parse(issue.getDescription()))); - for (var comment : comments) { - comment.setComment(renderer.render(parser.parse(comment.getComment()))); - } - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Component; -import de.uapcore.lightpit.entities.Project; -import de.uapcore.lightpit.entities.User; -import de.uapcore.lightpit.entities.Version; -import de.uapcore.lightpit.types.IssueCategory; -import de.uapcore.lightpit.types.IssueStatus; -import de.uapcore.lightpit.types.VersionStatus; - -import java.util.*; - -public class IssueEditView extends IssueDetailView { - private List projects = Collections.emptyList(); - private Set versionsUpcoming = new HashSet<>(); - private Set versionsRecent = new HashSet<>(); - private List users; - private List components; - - public List getProjects() { - return projects; - } - - public void setProjects(List projects) { - this.projects = projects; - } - - public Collection getVersionsUpcoming() { - return versionsUpcoming; - } - - public Collection getVersionsRecent() { - return versionsRecent; - } - - public void configureVersionSelectors(List versions) { - versionsRecent.clear(); - versionsUpcoming.clear(); - // keep the current selection, if any - versionsRecent.addAll(getIssue().getAffectedVersions()); - versionsUpcoming.addAll(getIssue().getResolvedVersions()); - for (var v : versions) { - if (v.getStatus().isReleased()) { - if (!v.getStatus().equals(VersionStatus.Deprecated)) - versionsRecent.add(v); - } else { - versionsUpcoming.add(v); - } - } - } - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } - - public List getComponents() { - return components; - } - - public void setComponents(List components) { - this.components = components; - } - - public IssueStatus[] getIssueStatus() { - return IssueStatus.values(); - } - - public IssueCategory[] getIssueCategory() { - return IssueCategory.values(); - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.entities.Project; -import de.uapcore.lightpit.entities.Version; - -import java.util.List; - -public class IssuesView { - private List issues; - private Project project; - private Version version; - - public List getIssues() { - return issues; - } - - public void setIssues(List issues) { - this.issues = issues; - } - - public Version getVersion() { - return version; - } - - public void setVersion(Version version) { - this.version = version; - } - - public Project getProject() { - return project; - } - - public void setProject(Project project) { - this.project = project; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import java.util.List; -import java.util.Locale; - -public class LanguageView { - - private List languages; - private Locale browserLanguage; - private Locale currentLanguage; - - - public List getLanguages() { - return languages; - } - - public void setLanguages(List languages) { - this.languages = languages; - } - - public Locale getBrowserLanguage() { - return browserLanguage; - } - - public void setBrowserLanguage(Locale browserLanguage) { - this.browserLanguage = browserLanguage; - } - - public Locale getCurrentLanguage() { - return currentLanguage; - } - - public void setCurrentLanguage(Locale currentLanguage) { - this.currentLanguage = currentLanguage; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.entities.IssueSummary; -import de.uapcore.lightpit.entities.Version; - -import java.util.List; - -public class ProjectDetails { - - private VersionInfo versionInfo = null; - - private List issues; - private IssueSummary issueSummary; - - public void updateDetails(List issues) { - this.issues = issues; - issueSummary = new IssueSummary(); - issues.forEach(issueSummary::add); - } - - public void updateVersionInfo(Version version) { - versionInfo = new VersionInfo(version); - versionInfo.collectIssues(issues); - } - - public List getIssues() { - return issues; - } - - public IssueSummary getIssueSummary() { - return issueSummary; - } - - public VersionInfo getVersionInfo() { - return versionInfo; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetailsView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetailsView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -public class ProjectDetailsView extends ProjectView { - - private final ProjectDetails projectDetails = new ProjectDetails(); - - public ProjectDetails getProjectDetails() { - return projectDetails; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Project; -import de.uapcore.lightpit.entities.User; - -import java.util.List; - -public class ProjectEditView extends ProjectView { - - private Project project; - private List users; - private String errorText; - - public Project getProject() { - return project; - } - - public void setProject(Project project) { - this.project = project; - } - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } - - public String getErrorText() { - return errorText; - } - - public void setErrorText(String errorText) { - this.errorText = errorText; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Component; -import de.uapcore.lightpit.entities.IssueSummary; -import de.uapcore.lightpit.entities.Project; -import de.uapcore.lightpit.entities.Version; - -import java.util.Collections; -import java.util.List; - -public class ProjectInfo { - - private final Project project; - private List versions = Collections.emptyList(); - private List components = Collections.emptyList(); - private IssueSummary issueSummary = new IssueSummary(); - - public ProjectInfo(Project project) { - this.project = project; - } - - public Project getProject() { - return project; - } - - public List getVersions() { - return versions; - } - - public void setVersions(List versions) { - this.versions = versions; - } - - public List getComponents() { - return components; - } - - public void setComponents(List components) { - this.components = components; - } - - public Version getLatestVersion() { - // expects versions to be sorted by status descending - for (var v : versions) { - if (v.getStatus().isReleased()) - return v; - } - return null; - } - - public Version getNextVersion() { - // expects versions to be sorted by status descending - Version next = null; - for (var v : versions) { - if (v.getStatus().isReleased()) - break; - next = v; - } - return next; - } - - public IssueSummary getIssueSummary() { - return issueSummary; - } - - public void setIssueSummary(IssueSummary issueSummary) { - this.issueSummary = issueSummary; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,79 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Component; -import de.uapcore.lightpit.entities.Version; - -import java.util.ArrayList; -import java.util.List; - -public class ProjectView { - - public static final int SELECTED_PAGE_ISSUES = 0; - public static final int SELECTED_PAGE_VERSIONS = 1; - public static final int SELECTED_PAGE_COMPONENTS = 2; - - // TODO: use new Filter class - - public static final Version ALL_VERSIONS = new Version(0,0); - public static final Version NO_VERSION = new Version(-1,0); - public static final Component ALL_COMPONENTS = new Component(0,0); - public static final Component NO_COMPONENT = new Component(-1,0); - - static { - ALL_VERSIONS.setNode("all-versions"); - NO_VERSION.setNode("no-version"); - ALL_COMPONENTS.setNode("all-components"); - NO_COMPONENT.setNode("no-component"); - } - - private final List projectList = new ArrayList<>(); - private ProjectInfo projectInfo; - private Version versionFilter; - private Component componentFilter; - - private int selectedPage = SELECTED_PAGE_ISSUES; - - public List getProjectList() { - return projectList; - } - - public ProjectInfo getProjectInfo() { - return projectInfo; - } - - public void setProjectInfo(ProjectInfo projectInfo) { - this.projectInfo = projectInfo; - } - - public int getSelectedPage() { - return selectedPage; - } - - public void setSelectedPage(int selectedPage) { - this.selectedPage = selectedPage; - } - - public Version getVersionFilter() { - return versionFilter; - } - - public void setVersionFilter(Version versionFilter) { - this.versionFilter = versionFilter; - } - - public Component getComponentFilter() { - return componentFilter; - } - - public void setComponentFilter(Component componentFilter) { - this.componentFilter = componentFilter; - } - - public boolean isProjectInfoPresent() { - return projectInfo != null; - } - - public boolean isEveryFilterValid() { - return projectInfo != null && versionFilter != null && componentFilter != null; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.User; - -public class UsersEditView { - private User user; - private String errorText; - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - public String getErrorText() { - return errorText; - } - - public void setErrorText(String errorText) { - this.errorText = errorText; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.User; - -import java.util.List; - -public class UsersView { - private List users; - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Version; -import de.uapcore.lightpit.types.VersionStatus; - -public class VersionEditView extends ProjectView { - private Version version; - private String errorText; - - public VersionEditView() { - setSelectedPage(SELECTED_PAGE_VERSIONS); - } - - public void setVersion(Version version) { - this.version = version; - } - - public Version getVersion() { - return version; - } - - public VersionStatus[] getVersionStatus() { - return VersionStatus.values(); - } - - public String getErrorText() { - return errorText; - } - - public void setErrorText(String errorText) { - this.errorText = errorText; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.entities.IssueSummary; -import de.uapcore.lightpit.entities.Version; - -import java.util.ArrayList; -import java.util.List; - -public class VersionInfo { - - private final Version version; - - private final IssueSummary reportedTotal = new IssueSummary(); - private final IssueSummary resolvedTotal = new IssueSummary(); - - private final List reported = new ArrayList<>(); - private final List resolved = new ArrayList<>(); - - public VersionInfo(Version version) { - this.version = version; - } - - public Version getVersion() { - return version; - } - - public void addReported(Issue issue) { - reportedTotal.add(issue); - reported.add(issue); - } - - public void addResolved(Issue issue) { - resolvedTotal.add(issue); - resolved.add(issue); - } - - public IssueSummary getReportedTotal() { - return reportedTotal; - } - - public IssueSummary getResolvedTotal() { - return resolvedTotal; - } - - public List getReported() { - return reported; - } - - public List getResolved() { - return resolved; - } - - public void collectIssues(List issues) { - for (Issue issue : issues) { - if (issue.getAffectedVersions().contains(version)) { - addReported(issue); - } - if (issue.getResolvedVersions().contains(version)) { - addResolved(issue); - } - } - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -package de.uapcore.lightpit.viewmodel; - -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.entities.Version; - -import java.util.ArrayList; -import java.util.List; - -public class VersionsView extends ProjectView { - - private List versionInfos = new ArrayList<>(); - - public VersionsView() { - setSelectedPage(SELECTED_PAGE_VERSIONS); - } - - public void update(List versions, List issues) { - versionInfos.clear(); - for (var version : versions) { - final var info = new VersionInfo(version); - info.collectIssues(issues); - versionInfos.add(info); - } - } - - public List getVersionInfos() { - return versionInfos; - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java --- a/src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -package de.uapcore.lightpit.viewmodel.util; - -import de.uapcore.lightpit.entities.Issue; -import de.uapcore.lightpit.types.IssueStatusPhase; - -import java.util.Arrays; -import java.util.Comparator; - -public class IssueSorter implements Comparator { - - public enum Field { - DONE, ETA, UPDATED - } - - public static class Criteria { - private Field field; - private boolean asc; - - public Criteria(Field field, boolean asc) { - this.field = field; - this.asc = asc; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || !obj.getClass().equals(Criteria.class)) - return false; - final var other = (Criteria)obj; - return other.field.equals(field) && other.asc == asc; - } - } - - private final Criteria[] criteria; - - public IssueSorter(Criteria ... criteria) { - this.criteria = criteria; - } - - private int compare(Issue left, Issue right, Criteria criteria) { - if (left.equals(right)) - return 0; - - int result; - switch (criteria.field) { - case DONE: - result = Boolean.compare( - left.getStatus().getPhase().equals(IssueStatusPhase.Companion.getDone()), - right.getStatus().getPhase().equals(IssueStatusPhase.Companion.getDone())); - break; - case ETA: - if (left.getEta() != null && right.getEta() != null) - result = left.getEta().compareTo(right.getEta()); - else if (left.getEta() == null && right.getEta() == null) - result = 0; - else - result = left.getEta() != null ? -1 : 1; - break; - case UPDATED: - result = left.getUpdated().compareTo(right.getUpdated()); - break; - default: - throw new UnsupportedOperationException(); - } - return criteria.asc ? result : -result; - } - - @Override - public int compare(Issue left, Issue right) { - for (var c : criteria) { - int r = compare(left, right, c); - if (r != 0) return r; - } - return 0; - } - - @Override - public boolean equals(Object o) { - if (o == null || !o.getClass().equals(IssueSorter.class)) - return false; - final var other = (IssueSorter) o; - return Arrays.equals(criteria, other.criteria); - } -} diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,182 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit + +import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME +import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.dao.createDataAccessObject +import java.sql.SQLException +import java.util.* +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +abstract class AbstractServlet : LoggingTrait, HttpServlet() { + + /** + * Contains the GET request mappings. + */ + private val getMappings = mutableMapOf() + + /** + * Contains the POST request mappings. + */ + private val postMappings = mutableMapOf() + + protected fun get(pattern: String, method: MappingMethod) { + getMappings[PathPattern(pattern)] = method + } + + protected fun post(pattern: String, method: MappingMethod) { + postMappings[PathPattern(pattern)] = method + } + + private fun notFound(http: HttpRequest, dao: DataAccessObject) { + http.response.sendError(HttpServletResponse.SC_NOT_FOUND) + } + + private fun findMapping( + mappings: Map, + req: HttpServletRequest + ): Pair { + val requestPath = sanitizedRequestPath(req) + val candidates = mappings.filter { it.key.matches(requestPath) } + return if (candidates.isEmpty()) { + Pair(PathPattern(requestPath), ::notFound) + } else { + if (candidates.size > 1) { + logger().warn("Ambiguous mapping for request path '{}'", requestPath) + } + candidates.entries.first().toPair() + } + } + + private fun invokeMapping( + mapping: Pair, + req: HttpServletRequest, + resp: HttpServletResponse, + dao: DataAccessObject + ) { + val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) + val method = mapping.second + logger().trace("invoke {}", method) + method(HttpRequest(req, resp, params), dao) + } + + private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" + + private fun doProcess( + req: HttpServletRequest, + resp: HttpServletResponse, + mappings: Map + ) { + val session = req.session + + // the very first thing to do is to force UTF-8 + req.characterEncoding = "UTF-8" + + // choose the requested language as session language (if available) or fall back to english, otherwise + if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { + val availableLanguages = availableLanguages() + val reqLocale = req.locale + val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() + session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale) + logger().debug( + "Setting language for new session {}: {}", session.id, sessionLocale.displayLanguage + ) + } else { + val sessionLocale = session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale + resp.locale = sessionLocale + logger().trace("Continuing session {} with language {}", session.id, sessionLocale) + } + + // set some internal request attributes + val http = HttpRequest(req, resp) + val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") + req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref) + req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) + req.getHeader("Referer")?.let { + // TODO: add a sanity check to avoid link injection + req.setAttribute(Constants.REQ_ATTR_REFERER, it) + } + + // if this is an error path, bypass the normal flow + if (fullPath.startsWith("/error/")) { + http.styleSheets = listOf("error") + http.render("error") + return + } + + // obtain a connection and create the data access objects + val dsp = req.servletContext.getAttribute(SC_ATTR_NAME) as DataSourceProvider + val dialect = dsp.dialect + val ds = dsp.dataSource + if (ds == null) { + resp.sendError( + HttpServletResponse.SC_SERVICE_UNAVAILABLE, + "JNDI DataSource lookup failed. See log for details." + ) + return + } + try { + ds.connection.use { connection -> + val dao = createDataAccessObject(dialect, connection) + try { + connection.autoCommit = false + invokeMapping(findMapping(mappings, req), req, resp, dao) + connection.commit() + } catch (ex: SQLException) { + logger().warn("Database transaction failed (Code {}): {}", ex.errorCode, ex.message) + logger().debug("Details: ", ex) + resp.sendError( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Unhandled Transaction Error - Code: " + ex.errorCode + ) + connection.rollback() + } + } + } catch (ex: SQLException) { + logger().error("Severe Database Exception (Code {}): {}", ex.errorCode, ex.message) + logger().debug("Details: ", ex) + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode) + } + } + + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { + doProcess(req, resp, getMappings) + } + + override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { + doProcess(req, resp, postMappings) + } + + protected fun availableLanguages(): List { + val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList() + val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() } + return if (locales.isEmpty()) listOf(Locale.ENGLISH) else locales + } + +} diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/Constants.kt --- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt Fri Apr 02 11:59:14 2021 +0200 @@ -52,7 +52,7 @@ const val CTX_ATTR_DB_DIALECT = "db-dialect" /** - * Key for the request attribute containing the optional navigation menu jsp. + * Key for the request attribute containing the optional navigation menu. */ const val REQ_ATTR_NAVIGATION = "navMenu" @@ -88,6 +88,11 @@ const val REQ_ATTR_REDIRECT_LOCATION = "redirectLocation" /** + * Key for the optional return link based on the referer header. + */ + const val REQ_ATTR_REFERER = "returnLink" + + /** * Key for the current language selection within the session. */ const val SESSION_ATTR_LANGUAGE = "language" diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt --- a/src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt Fri Apr 02 11:59:14 2021 +0200 @@ -61,12 +61,12 @@ /** * The attribute name in the Servlet context under which an instance of this class can be found. */ - val SC_ATTR_NAME = "lightpit.service.DataSourceProvider" + const val SC_ATTR_NAME = "lightpit.service.DataSourceProvider" /** * Timeout in seconds for the validation test. */ - private val DB_TEST_TIMEOUT = 10 + private const val DB_TEST_TIMEOUT = 10 /** * The default schema to test against when validating the connection. @@ -74,12 +74,12 @@ * * @see Constants.CTX_ATTR_DB_SCHEMA */ - private val DB_DEFAULT_SCHEMA = "lightpit" + private const val DB_DEFAULT_SCHEMA = "lightpit" /** * The JNDI resource name for the data source. */ - private val DS_JNDI_NAME = "jdbc/lightpit/app" + private const val DS_JNDI_NAME = "jdbc/lightpit/app" } private fun checkConnection(ds: DataSource, testSchema: String) { diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Fri Apr 02 11:59:14 2021 +0200 @@ -25,45 +25,106 @@ package de.uapcore.lightpit +import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.viewmodel.NavMenu +import de.uapcore.lightpit.viewmodel.View +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import javax.servlet.http.HttpSession import kotlin.math.min -/** - * Maps requests to methods. - * - * This annotation is used to annotate methods within classes which - * override [AbstractServlet]. - */ -@MustBeDocumented -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.FUNCTION) -annotation class RequestMapping( +typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit +typealias PathParameters = Map + +class HttpRequest( + val request: HttpServletRequest, + val response: HttpServletResponse, + val pathParams: PathParameters = emptyMap() +) { + val session: HttpSession = request.session + + val remoteUser: String? = request.remoteUser + + /** + * The name of the content page. + * + * @see Constants#REQ_ATTR_CONTENT_PAGE + */ + var contentPage = "" + set(value) { + field = value + request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value)) + } + + /** + * A list of additional style sheets. + * + * @see Constants#REQ_ATTR_STYLESHEET + */ + var styleSheets = emptyList() + set(value) { + field = value + request.setAttribute(Constants.REQ_ATTR_STYLESHEET, + value.map { it.withExt(".css") } + ) + } /** - * Specifies the HTTP method. + * The name of the navigation menu JSP. * - * @return the HTTP method handled by the annotated Java method + * @see Constants#REQ_ATTR_NAVIGATION */ - val method: HttpMethod, + var navigationMenu: NavMenu? = null + set(value) { + field = value + request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu) + } + + var redirectLocation = "" + set(value) { + field = value + request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value) + } /** - * Specifies the request path relative to the module path. - * The trailing slash is important. - * A node may start with a dollar ($) sign. - * This part of the path is then treated as an path parameter. - * Path parameters can be obtained by including the [PathParameters] type in the signature. + * The view object. * - * @return the request path the annotated method should handle + * @see Constants#REQ_ATTR_VIEWMODEL + */ + var view: View? = null + set(value) { + field = value + request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value) + } + + /** + * The base path of this application. */ - val requestPath: String = "/" -) + val baseHref get() = "${request.scheme}://${request.serverName}:${request.serverPort}${request.contextPath}/" + + private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext) + private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp") + + fun param(name: String): String? = request.getParameter(name) + fun paramArray(name: String): Array = request.getParameterValues(name) ?: emptyArray() -class PathParameters : HashMap() + fun render(page: String? = null) { + page?.let { contentPage = it } + request.getRequestDispatcher(jspPath("site")).forward(request, response) + } + + fun renderCommit(location: String? = null) { + location?.let { redirectLocation = it } + contentPage = Constants.JSP_COMMIT_SUCCESSFUL + render() + } +} /** * A path pattern optionally containing placeholders. * * The special directories . and .. are disallowed in the pattern. - * Placeholders start with a $ sign. + * Placeholders start with a % sign. * * @param pattern the pattern */ @@ -91,7 +152,7 @@ for (i in nodePatterns.indices) { val pattern = nodePatterns[i] val node = nodes[i] - if (pattern.startsWith("$")) continue + if (pattern.startsWith("%")) continue if (pattern != node) return false } return true @@ -106,12 +167,12 @@ * @see .matches */ fun obtainPathParameters(path: String): PathParameters { - val params = PathParameters() + val params = mutableMapOf() val nodes = parse(path) for (i in 0 until min(nodes.size, nodePatterns.size)) { val pattern = nodePatterns[i] val node = nodes[i] - if (pattern.startsWith("$")) { + if (pattern.startsWith("%")) { params[pattern.substring(1)] = node } } @@ -121,8 +182,8 @@ override fun hashCode(): Int { val str = StringBuilder() for (node in nodePatterns) { - if (node.startsWith("$")) { - str.append("/$") + if (node.startsWith("%")) { + str.append("/%") } else { str.append('/') str.append(node) @@ -138,7 +199,7 @@ for (i in nodePatterns.indices) { val left = nodePatterns[i] val right = other.nodePatterns[i] - if (left.startsWith("$") && right.startsWith("$")) continue + if (left.startsWith("%") && right.startsWith("%")) continue if (left != right) return false } return true diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/dao/DaoFactory.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DaoFactory.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.dao + +import de.uapcore.lightpit.DataSourceProvider +import java.sql.Connection + +fun createDataAccessObject( + dialect: DataSourceProvider.Dialect, + connection: Connection +): DataAccessObject = + when (dialect) { + DataSourceProvider.Dialect.Postgres -> PostgresDataAccessObject(connection) + } \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Fri Apr 02 11:59:14 2021 +0200 @@ -26,7 +26,10 @@ package de.uapcore.lightpit.dao import de.uapcore.lightpit.entities.* -import de.uapcore.lightpit.filter.IssueFilter +import de.uapcore.lightpit.util.IssueFilter +import de.uapcore.lightpit.viewmodel.ComponentSummary +import de.uapcore.lightpit.viewmodel.IssueSummary +import de.uapcore.lightpit.viewmodel.VersionSummary interface DataAccessObject { fun listUsers(): List @@ -35,18 +38,29 @@ fun insertUser(user: User) fun updateUser(user: User) + /** + * Lists all versions of the specified [project]. + * + * The list is first ordered by the ordinal of the version and + * then by name, both descending. + */ fun listVersions(project: Project): List + fun listVersionSummaries(project: Project): List fun findVersion(id: Int): Version? fun findVersionByNode(project: Project, node: String): Version? fun insertVersion(version: Version) fun updateVersion(version: Version) fun listComponents(project: Project): List + fun listComponentSummaries(project: Project): List fun findComponent(id: Int): Component? fun findComponentByNode(project: Project, node: String): Component? fun insertComponent(component: Component) fun updateComponent(component: Component) + /** + * Lists all projects ordered by name. + */ fun listProjects(): List fun findProject(id: Int): Project? fun findProjectByNode(node: String): Project? @@ -57,7 +71,7 @@ fun listIssues(filter: IssueFilter): List fun findIssue(id: Int): Issue? - fun insertIssue(issue: Issue) + fun insertIssue(issue: Issue): Int fun updateIssue(issue: Issue) fun listComments(issue: Issue): List diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Fri Apr 02 11:59:14 2021 +0200 @@ -26,8 +26,11 @@ package de.uapcore.lightpit.dao import de.uapcore.lightpit.entities.* -import de.uapcore.lightpit.filter.* import de.uapcore.lightpit.types.WebColor +import de.uapcore.lightpit.util.* +import de.uapcore.lightpit.viewmodel.ComponentSummary +import de.uapcore.lightpit.viewmodel.IssueSummary +import de.uapcore.lightpit.viewmodel.VersionSummary import java.sql.Connection import java.sql.PreparedStatement import java.sql.ResultSet @@ -129,15 +132,19 @@ // // + + private fun obtainVersion(rs: ResultSet) = + Version(rs.getInt("versionid"), rs.getInt("project")).apply { + name = rs.getString("name") + node = rs.getString("node") + ordinal = rs.getInt("ordinal") + status = rs.getEnum("status") + } + private fun selectVersions(stmt: PreparedStatement) = sequence { stmt.executeQuery().use { rs -> while (rs.next()) { - yield(Version(rs.getInt("versionid"), rs.getInt("project")).apply { - name = rs.getString("name") - node = rs.getString("node") - ordinal = rs.getInt("ordinal") - status = rs.getEnum("status") - }) + yield(obtainVersion(rs)) } } } @@ -163,6 +170,36 @@ """ ) } + private val stmtVersionSummaries by lazy { + connection.prepareStatement( + """ + with version_map(issueid, versionid, isresolved) as ( + select issueid, versionid, 1 + from lpit_issue_resolved_version + union + select issueid, versionid, 0 + from lpit_issue_affected_version + ), + issues as ( + select versionid, phase, isresolved, count(issueid) as total + from lpit_issue + join version_map using (issueid) + join lpit_issue_phases using (status) + group by versionid, phase, isresolved + ), + summary as ( + select versionid, phase, isresolved, total + from lpit_version v + left join issues using (versionid) + where v.project = ? + ) + select versionid, project, name, node, ordinal, status, phase, isresolved, total + from lpit_version + join summary using (versionid) + order by ordinal, name + """ + ) + } private val stmtVersionByID by lazy { connection.prepareStatement( """${versionQuery} @@ -199,6 +236,27 @@ return selectVersions(stmtVersions).toList() } + override fun listVersionSummaries(project: Project): List { + stmtVersionSummaries.setInt(1, project.id) + return sequence { + stmtVersionSummaries.executeQuery().use { rs -> + while (rs.next()) { + val versionSummary = VersionSummary(obtainVersion(rs)) + val phase = rs.getInt("phase") + val total = rs.getInt("total") + val issueSummary = + if (rs.getBoolean("isresolved")) versionSummary.resolvedTotal else versionSummary.reportedTotal + when (phase) { + 0 -> issueSummary.open = total + 1 -> issueSummary.active = total + 2 -> issueSummary.done = total + } + yield(versionSummary) + } + } + }.toList() + } + override fun findVersion(id: Int): Version? { stmtVersionByID.setInt(1, id) return selectVersions(stmtVersionByID).firstOrNull() @@ -224,21 +282,25 @@ // // + + private fun obtainComponent(rs: ResultSet): Component = + Component(rs.getInt("id"), rs.getInt("project")).apply { + name = rs.getString("name") + node = rs.getString("node") + color = try { + WebColor(rs.getString("color")) + } catch (ex: IllegalArgumentException) { + WebColor("000000") + } + ordinal = rs.getInt("ordinal") + description = rs.getString("description") + lead = selectUserInfo(rs) + } + private fun selectComponents(stmt: PreparedStatement) = sequence { stmt.executeQuery().use { rs -> while (rs.next()) { - yield(Component(rs.getInt("id"), rs.getInt("project")).apply { - name = rs.getString("name") - node = rs.getString("node") - color = try { - WebColor(rs.getString("color")) - } catch (ex: IllegalArgumentException) { - WebColor("000000") - } - ordinal = rs.getInt("ordinal") - description = rs.getString("description") - lead = selectUserInfo(rs) - }) + yield(obtainComponent(rs)) } } } @@ -272,6 +334,30 @@ """ ) } + private val stmtComponentSummaries by lazy { + connection.prepareStatement( + """ + with issues as ( + select component, phase, count(issueid) as total + from lpit_issue + join lpit_issue_phases using (status) + group by component, phase + ), + summary as ( + select c.id, phase, total + from lpit_component c + left join issues i on c.id = i.component + where c.project = ? + ) + select c.id, project, name, node, color, ordinal, description, + userid, username, givenname, lastname, mail, phase, total + from lpit_component c + left join lpit_user on lead = userid + join summary s on c.id = s.id + order by ordinal, name + """ + ) + } private val stmtComponentById by lazy { connection.prepareStatement( """${componentQuery} @@ -305,6 +391,25 @@ return selectComponents(stmtComponents).toList() } + override fun listComponentSummaries(project: Project): List { + stmtComponentSummaries.setInt(1, project.id) + return sequence { + stmtComponentSummaries.executeQuery().use { rs -> + while (rs.next()) { + val componentSummary = ComponentSummary(obtainComponent(rs)) + val phase = rs.getInt("phase") + val total = rs.getInt("total") + when (phase) { + 0 -> componentSummary.issueSummary.open = total + 1 -> componentSummary.issueSummary.active = total + 2 -> componentSummary.issueSummary.done = total + } + yield(componentSummary) + } + } + }.toList() + } + override fun findComponent(id: Int): Component? { stmtComponentById.setInt(1, id) return selectComponents(stmtComponentById).firstOrNull() @@ -471,7 +576,7 @@ node = rs.getString("componentnode") } } - val issue = Issue(rs.getInt("issueid"), proj, comp).apply { + val issue = Issue(rs.getInt("issueid"), proj).apply { component = comp status = rs.getEnum("status") category = rs.getEnum("category") @@ -672,14 +777,15 @@ } } - override fun insertIssue(issue: Issue) { + override fun insertIssue(issue: Issue): Int { val col = setIssueFields(stmtInsertIssue, issue) stmtInsertIssue.setInt(col, issue.project.id) - stmtInsertIssue.executeQuery().use { rs -> + val id = stmtInsertIssue.executeQuery().use { rs -> rs.next() - issue.id = rs.getInt(1) + rs.getInt(1) } insertVersionInfo(issue) + return id } override fun updateIssue(issue: Issue) { diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/entities/Component.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/Component.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Component.kt Fri Apr 02 11:59:14 2021 +0200 @@ -27,11 +27,11 @@ import de.uapcore.lightpit.types.WebColor -data class Component(override val id: Int, var projectid: Int) : Entity { +data class Component(override val id: Int, val projectid: Int) : Entity, HasNode { var name: String = "" - var node: String = name + override var node: String = name + var ordinal = 0 var color = WebColor("000000") - var ordinal = 0 var description: String? = null var lead: User? = null } \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt Fri Apr 02 11:59:14 2021 +0200 @@ -27,4 +27,8 @@ interface Entity { val id: Int -} \ No newline at end of file +} + +interface HasNode { + val node: String +} diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Fri Apr 02 11:59:14 2021 +0200 @@ -32,13 +32,13 @@ import java.sql.Timestamp import java.time.Instant -data class Issue(override var id: Int, var project: Project, var component: Component? = null) : Entity { - +data class Issue(override val id: Int, var project: Project) : Entity { + var component: Component? = null var status = IssueStatus.InSpecification var category = IssueCategory.Feature var subject: String = "" - var description: String? = null + var description: String = "" var assignee: User? = null var created: Timestamp = Timestamp.from(Instant.now()) diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/entities/IssueSummary.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/IssueSummary.kt Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -/* - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package de.uapcore.lightpit.entities - -import de.uapcore.lightpit.types.IssueStatusPhase -import kotlin.math.roundToInt - -class IssueSummary { - var open = 0 - var active = 0 - var done = 0 - - val total get() = open + active + done - - val openPercent get() = 100 - activePercent - donePercent - val activePercent get() = if (total > 0) (100f * active / total).roundToInt() else 0 - val donePercent get() = if (total > 0) (100f * done / total).roundToInt() else 100 - - /** - * Adds the specified issue to the summary by incrementing the respective counter. - * @param issue the issue - */ - fun add(issue: Issue) { - when (issue.status.phase) { - IssueStatusPhase.Open -> open++ - IssueStatusPhase.WorkInProgress -> active++ - IssueStatusPhase.Done -> done++ - } - } -} \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/entities/Project.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt Fri Apr 02 11:59:14 2021 +0200 @@ -25,9 +25,9 @@ package de.uapcore.lightpit.entities -data class Project(override val id: Int) : Entity { +data class Project(override val id: Int) : Entity, HasNode { var name: String = "" - var node: String = name + override var node: String = name var ordinal = 0 var description: String? = null var repoUrl: String? = null diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/entities/Version.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/Version.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Version.kt Fri Apr 02 11:59:14 2021 +0200 @@ -27,9 +27,9 @@ import de.uapcore.lightpit.types.VersionStatus -data class Version(override val id: Int, var projectid: Int) : Entity, Comparable { +data class Version(override val id: Int, val projectid: Int) : Entity, HasNode, Comparable { var name: String = "" - var node = name + override var node = name var ordinal = 0 var status = VersionStatus.Future diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/filter/Filter.kt --- a/src/main/kotlin/de/uapcore/lightpit/filter/Filter.kt Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -/* - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package de.uapcore.lightpit.filter - -sealed class Filter -class AllFilter : Filter() -class NoneFilter : Filter() -data class SpecificFilter(val obj: T) : Filter() -data class RangeFilter(val lower: T, val upper: T) : Filter() where T : Comparable diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/filter/IssueFilter.kt --- a/src/main/kotlin/de/uapcore/lightpit/filter/IssueFilter.kt Sat Jan 23 14:47:59 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -/* - * Copyright 2021 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package de.uapcore.lightpit.filter - -import de.uapcore.lightpit.entities.Component -import de.uapcore.lightpit.entities.Project -import de.uapcore.lightpit.entities.Version - -data class IssueFilter( - val project: Filter = AllFilter(), - val version: Filter = AllFilter(), - val component: Filter = AllFilter() -) diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/servlet/ErrorServlet.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ErrorServlet.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.servlet + +import de.uapcore.lightpit.AbstractServlet +import javax.servlet.annotation.WebServlet + +@WebServlet(urlPatterns = ["/error/*"]) +class ErrorServlet : AbstractServlet() \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/servlet/LanguageServlet.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/LanguageServlet.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.servlet + +import de.uapcore.lightpit.AbstractServlet +import de.uapcore.lightpit.Constants +import de.uapcore.lightpit.HttpRequest +import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.viewmodel.LanguageView +import java.util.* +import javax.servlet.annotation.WebServlet + +@WebServlet(urlPatterns = ["/language/*"]) +class LanguageServlet : AbstractServlet() { + + init { + get("/", this::viewLanguages) + post("/", this::selectLanguage) + } + + private fun viewLanguages(http: HttpRequest, dao: DataAccessObject) { + with(http) { + view = LanguageView( + availableLanguages(), + session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale, + request.locale + ) + styleSheets = listOf("language") + render("language") + } + } + + private fun selectLanguage(http: HttpRequest, dao: DataAccessObject) { + val lang = http.param("language") + if (lang != null) { + val locale = Locale.forLanguageTag(lang) + if (!locale.language.isNullOrBlank()) { + http.response.locale = locale + http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) + } + } + + viewLanguages(http, dao) + } + +} \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,522 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.servlet + +import de.uapcore.lightpit.AbstractServlet +import de.uapcore.lightpit.HttpRequest +import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.entities.* +import de.uapcore.lightpit.types.IssueCategory +import de.uapcore.lightpit.types.IssueStatus +import de.uapcore.lightpit.types.VersionStatus +import de.uapcore.lightpit.types.WebColor +import de.uapcore.lightpit.util.AllFilter +import de.uapcore.lightpit.util.IssueFilter +import de.uapcore.lightpit.util.SpecificFilter +import de.uapcore.lightpit.viewmodel.* +import java.sql.Date +import javax.servlet.annotation.WebServlet + +@WebServlet(urlPatterns = ["/projects/*"]) +class ProjectServlet : AbstractServlet() { + + init { + get("/", this::projects) + get("/%project", this::project) + get("/%project/issues/%version/%component/", this::project) + get("/%project/edit", this::projectForm) + get("/-/create", this::projectForm) + post("/-/commit", this::projectCommit) + + get("/%project/versions/", this::versions) + get("/%project/versions/%version/edit", this::versionForm) + get("/%project/versions/-/create", this::versionForm) + post("/%project/versions/-/commit", this::versionCommit) + + get("/%project/components/", this::components) + get("/%project/components/%component/edit", this::componentForm) + get("/%project/components/-/create", this::componentForm) + post("/%project/components/-/commit", this::componentCommit) + + get("/%project/issues/%version/%component/%issue", this::issue) + get("/%project/issues/%version/%component/%issue/edit", this::issueForm) + get("/%project/issues/%version/%component/%issue/comment", this::issueComment) + get("/%project/issues/%version/%component/-/create", this::issueForm) + get("/%project/issues/%version/%component/-/commit", this::issueCommit) + } + + fun projects(http: HttpRequest, dao: DataAccessObject) { + val projects = dao.listProjects() + val projectInfos = projects.map { + ProjectInfo( + project = it, + versions = dao.listVersions(it), + components = emptyList(), // not required in this view + issueSummary = dao.collectIssueSummary(it) + ) + } + + with(http) { + view = ProjectsView(projectInfos) + navigationMenu = projectNavMenu(projects) + styleSheets = listOf("projects") + render("projects") + } + } + + private fun activeProjectNavMenu( + projects: List, + projectInfo: ProjectInfo, + selectedVersion: Version? = null, + selectedComponent: Component? = null + ) = + projectNavMenu( + projects, + projectInfo.versions, + projectInfo.components, + projectInfo.project, + selectedVersion, + selectedComponent + ) + + sealed class LookupResult { + class NotFound : LookupResult() + data class Found(val elem: T?) : LookupResult() + } + + private fun HttpRequest.lookupPathParam(paramName: String, list: List): LookupResult { + val node = pathParams[paramName] + return if (node == null || node == "-") { + LookupResult.Found(null) + } else { + val result = list.find { it.node == node } + if (result == null) { + LookupResult.NotFound() + } else { + LookupResult.Found(result) + } + } + } + + private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? { + val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null + + val versions: List = dao.listVersions(project) + val components: List = dao.listComponents(project) + + return ProjectInfo( + project, + versions, + components, + dao.collectIssueSummary(project) + ) + } + + private fun sanitizeNode(name: String): String { + val san = name.replace(Regex("[/\\\\]"), "-") + return if (san.startsWith(".")) { + "v$san" + } else { + san + } + } + + data class PathInfos( + val projectInfo: ProjectInfo, + val version: Version?, + val component: Component? + ) { + val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/") + } + + private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return null + } + + val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) { + is LookupResult.NotFound -> { + http.response.sendError(404) + return null + } + is LookupResult.Found -> { + result.elem + } + } + val component = when (val result = http.lookupPathParam("component", projectInfo.components)) { + is LookupResult.NotFound -> { + http.response.sendError(404) + return null + } + is LookupResult.Found -> { + result.elem + } + } + + return PathInfos(projectInfo, version, component) + } + + fun project(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.run { + val issues = dao.listIssues(IssueFilter( + project = SpecificFilter(projectInfo.project), + version = version?.let { SpecificFilter(it) } ?: AllFilter(), + component = component?.let { SpecificFilter(it) } ?: AllFilter() + )) + + with(http) { + view = ProjectDetails(projectInfo, issues, version, component) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo, + version, + component + ) + styleSheets = listOf("projects") + render("project-details") + } + } + } + + fun projectForm(http: HttpRequest, dao: DataAccessObject) { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return + } + + with(http) { + view = ProjectEditView(projectInfo.project, dao.listUsers()) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo + ) + styleSheets = listOf("projects") + render("project-form") + } + } + + fun projectCommit(http: HttpRequest, dao: DataAccessObject) { + // TODO: replace defaults with throwing validator exceptions + val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply { + name = http.param("name") ?: "" + node = http.param("node") ?: "" + description = http.param("description") ?: "" + ordinal = http.param("ordinal")?.toIntOrNull() ?: 0 + repoUrl = http.param("repoUrl") ?: "" + owner = (http.param("owner")?.toIntOrNull() ?: -1).let { + if (it < 0) null else dao.findUser(it) + } + // intentional defaults + if (node.isBlank()) node = name + // sanitizing + node = sanitizeNode(node) + } + + if (project.id < 0) { + dao.insertProject(project) + } else { + dao.updateProject(project) + } + + http.renderCommit("projects/${project.node}") + } + + fun versions(http: HttpRequest, dao: DataAccessObject) { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return + } + + with(http) { + view = VersionsView( + projectInfo, + dao.listVersionSummaries(projectInfo.project) + ) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo + ) + styleSheets = listOf("projects") + render("versions") + } + } + + fun versionForm(http: HttpRequest, dao: DataAccessObject) { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return + } + + val version: Version + when (val result = http.lookupPathParam("version", projectInfo.versions)) { + is LookupResult.NotFound -> { + http.response.sendError(404) + return + } + is LookupResult.Found -> { + version = result.elem ?: Version(-1, projectInfo.project.id) + } + } + + with(http) { + view = VersionEditView(projectInfo, version) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo, + selectedVersion = version + ) + styleSheets = listOf("projects") + render("version-form") + } + } + + fun versionCommit(http: HttpRequest, dao: DataAccessObject) { + val id = http.param("id")?.toIntOrNull() + val projectid = http.param("projectid")?.toIntOrNull() ?: -1 + val project = dao.findProject(projectid) + if (id == null || project == null) { + http.response.sendError(400) + return + } + + // TODO: replace defaults with throwing validator exceptions + val version = Version(id, projectid).apply { + name = http.param("name") ?: "" + node = http.param("node") ?: "" + ordinal = http.param("ordinal")?.toIntOrNull() ?: 0 + status = http.param("status")?.let(VersionStatus::valueOf) ?: VersionStatus.Future + // intentional defaults + if (node.isBlank()) node = name + // sanitizing + node = sanitizeNode(node) + } + + if (id < 0) { + dao.insertVersion(version) + } else { + dao.updateVersion(version) + } + + http.renderCommit("projects/${project.node}/versions/") + } + + fun components(http: HttpRequest, dao: DataAccessObject) { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return + } + + with(http) { + view = ComponentsView( + projectInfo, + dao.listComponentSummaries(projectInfo.project) + ) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo + ) + styleSheets = listOf("projects") + render("components") + } + } + + fun componentForm(http: HttpRequest, dao: DataAccessObject) { + val projectInfo = obtainProjectInfo(http, dao) + if (projectInfo == null) { + http.response.sendError(404) + return + } + + val component: Component + when (val result = http.lookupPathParam("component", projectInfo.components)) { + is LookupResult.NotFound -> { + http.response.sendError(404) + return + } + is LookupResult.Found -> { + component = result.elem ?: Component(-1, projectInfo.project.id) + } + } + + with(http) { + view = ComponentEditView(projectInfo, component, dao.listUsers()) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo, + selectedComponent = component + ) + styleSheets = listOf("projects") + render("component-form") + } + } + + fun componentCommit(http: HttpRequest, dao: DataAccessObject) { + val id = http.param("id")?.toIntOrNull() + val projectid = http.param("projectid")?.toIntOrNull() ?: -1 + val project = dao.findProject(projectid) + if (id == null || project == null) { + http.response.sendError(400) + return + } + + // TODO: replace defaults with throwing validator exceptions + val component = Component(id, projectid).apply { + name = http.param("name") ?: "" + node = http.param("node") ?: "" + ordinal = http.param("ordinal")?.toIntOrNull() ?: 0 + color = WebColor(http.param("color") ?: "#000000") + description = http.param("description") + lead = (http.param("lead")?.toIntOrNull() ?: -1).let { + if (it < 0) null else dao.findUser(it) + } + // intentional defaults + if (node.isBlank()) node = name + // sanitizing + node = sanitizeNode(node) + } + + if (id < 0) { + dao.insertComponent(component) + } else { + dao.updateComponent(component) + } + + http.renderCommit("projects/${project.node}/components/") + } + + fun issue(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.run { + val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) + if (issue == null) { + http.response.sendError(404) + return + } + + val comments = dao.listComments(issue) + + with(http) { + view = IssueDetailView(issue, comments, projectInfo.project, version, component) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo, + version, + component + ) + styleSheets = listOf("projects") + render("issue-view") + } + } + } + + fun issueForm(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.run { + val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue( + -1, + projectInfo.project, + ) + + // pre-select component, if available in the path info + issue.component = component + + with(http) { + view = IssueEditView( + issue, + projectInfo.versions, + projectInfo.components, + dao.listUsers(), + projectInfo.project, + version, + component + ) + navigationMenu = activeProjectNavMenu( + dao.listProjects(), + projectInfo, + version, + component + ) + styleSheets = listOf("projects") + render("issue-form") + } + } + } + + fun issueComment(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.run { + val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) + if (issue == null) { + http.response.sendError(404) + return + } + + // TODO: throw validator exception instead of using a default + val comment = IssueComment(-1, issue.id).apply { + author = http.remoteUser?.let { dao.findUserByName(it) } + comment = http.param("comment") ?: "" + } + + dao.insertComment(comment) + + http.renderCommit("${issuesHref}${issue.id}") + } + } + + fun issueCommit(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.run { + // TODO: throw validator exception instead of using defaults + val issue = Issue( + http.param("id")?.toIntOrNull() ?: -1, + projectInfo.project + ).apply { + component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1) + category = IssueCategory.valueOf(http.param("category") ?: "") + status = IssueStatus.valueOf(http.param("status") ?: "") + subject = http.param("subject") ?: "" + description = http.param("description") ?: "" + assignee = http.param("assignee")?.toIntOrNull()?.let { + when (it) { + -1 -> null + -2 -> component?.lead + else -> dao.findUser(it) + } + } + eta = http.param("eta")?.let { Date.valueOf(it) } + + affectedVersions = http.paramArray("affected") + .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } } + resolvedVersions = http.paramArray("resolved") + .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } } + } + + http.renderCommit("${issuesHref}${issue.id}") + } + } +} \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.servlet + +import de.uapcore.lightpit.AbstractServlet +import de.uapcore.lightpit.HttpRequest +import de.uapcore.lightpit.LoggingTrait +import de.uapcore.lightpit.dao.DataAccessObject +import de.uapcore.lightpit.entities.User +import de.uapcore.lightpit.logger +import de.uapcore.lightpit.viewmodel.UserEditView +import de.uapcore.lightpit.viewmodel.UsersView +import javax.servlet.annotation.WebServlet + +@WebServlet(urlPatterns = ["/users/*"]) +class UsersServlet : AbstractServlet(), LoggingTrait { + + init { + get("/", this::index) + get("/-/create", this::create) + get("/%userid/edit", this::edit) + post("/-/commit", this::commit) + } + + private val list = "users" + private val form = "user-form" + + fun index(http: HttpRequest, dao: DataAccessObject) { + with(http) { + view = UsersView(dao.listUsers()) + render(list) + } + } + + fun create(http: HttpRequest, dao: DataAccessObject) { + with(http) { + view = UserEditView(User(-1)) + render(form) + } + } + + fun edit(http: HttpRequest, dao: DataAccessObject) { + val id = http.pathParams["userid"]?.toIntOrNull() + if (id == null) { + http.response.sendError(404) + } else { + val user = dao.findUser(id) + if (user == null) { + http.response.sendError(404) + } else { + with(http) { + view = UserEditView(user) + render(form) + } + } + } + } + + fun commit(http: HttpRequest, dao: DataAccessObject) { + val id = http.param("userid")?.toIntOrNull() + if (id == null) { + http.response.sendError(400) + return + } + + val user = User(id) + with(user) { + username = http.param("username") ?: "" + givenname = http.param("givenname") + lastname = http.param("lastname") + mail = http.param("mail") + } + + if (dao.findUserByName(user.username) != null) { + with(http) { + view = UserEditView(user).apply { errorText = "validation.username.unique" } + } + } + + if (user.id > 0) { + logger().info("Update user ${user.username} with id ${user.id}.") + dao.updateUser(user) + } else { + logger().info("Insert user ${user.username}.") + dao.insertUser(user) + } + http.renderCommit("users/") + } +} \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt --- a/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt Fri Apr 02 11:59:14 2021 +0200 @@ -27,7 +27,7 @@ /** - * Represents a web color in hexadezimal representation. + * Represents a web color in hexadecimal representation. * @param arg the 6 digits hex string optionally preceded by a hash symbol */ class WebColor(arg: String) { diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/util/Filter.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/util/Filter.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.util + +sealed class Filter +class AllFilter : Filter() +class NoneFilter : Filter() +data class SpecificFilter(val obj: T) : Filter() +data class RangeFilter(val lower: T, val upper: T) : Filter() where T : Comparable diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/util/Issues.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/util/Issues.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.util + +import de.uapcore.lightpit.entities.Component +import de.uapcore.lightpit.entities.Issue +import de.uapcore.lightpit.entities.Project +import de.uapcore.lightpit.entities.Version +import de.uapcore.lightpit.types.IssueStatusPhase + +data class IssueFilter( + val project: Filter = AllFilter(), + val version: Filter = AllFilter(), + val component: Filter = AllFilter() +) + +data class IssueSorter(val criteria: List) : Comparator { + enum class Field { + DONE, ETA, UPDATED + } + + data class Criteria(val field: Field, val asc: Boolean) + + override fun compare(left: Issue, right: Issue): Int { + if (left == right) { + return 0; + } + for (c in criteria) { + val result = when (c.field) { + Field.DONE -> (left.status.phase == IssueStatusPhase.Done).compareTo(right.status.phase == IssueStatusPhase.Done) + Field.ETA -> { + val l = left.eta + val r = right.eta + if (l == null && r == null) 0 + else if (l == null) 1 + else if (r == null) -1 + else l.compareTo(r) + } + Field.UPDATED -> left.updated.compareTo(right.updated) + } + if (result != 0) { + return if (c.asc) result else -result + } + } + return 0 + } +} + diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/Components.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Components.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import de.uapcore.lightpit.entities.Component +import de.uapcore.lightpit.entities.User + +class ComponentSummary( + val component: Component, +) { + val issueSummary = IssueSummary() +} + +class ComponentsView( + val projectInfo: ProjectInfo, + val componentInfos: List +) : View() + +class ComponentEditView( + val projectInfo: ProjectInfo, + val component: Component, + val users: List +) : EditView() diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,118 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension +import com.vladsch.flexmark.ext.tables.TablesExtension +import com.vladsch.flexmark.html.HtmlRenderer +import com.vladsch.flexmark.parser.Parser +import com.vladsch.flexmark.util.data.MutableDataSet +import de.uapcore.lightpit.entities.* +import de.uapcore.lightpit.types.IssueCategory +import de.uapcore.lightpit.types.IssueStatus +import de.uapcore.lightpit.types.IssueStatusPhase +import de.uapcore.lightpit.types.VersionStatus +import kotlin.math.roundToInt + +class IssueSummary { + var open = 0 + var active = 0 + var done = 0 + + val total get() = open + active + done + + val openPercent get() = 100 - activePercent - donePercent + val activePercent get() = if (total > 0) (100f * active / total).roundToInt() else 0 + val donePercent get() = if (total > 0) (100f * done / total).roundToInt() else 100 + + /** + * Adds the specified issue to the summary by incrementing the respective counter. + * @param issue the issue + */ + fun add(issue: Issue) { + when (issue.status.phase) { + IssueStatusPhase.Open -> open++ + IssueStatusPhase.WorkInProgress -> active++ + IssueStatusPhase.Done -> done++ + } + } +} + +class IssueDetailView( + val issue: Issue, + val comments: List, + val project: Project, + val version: Version? = null, + val component: Component? = null +) : View() { + + init { + val options = MutableDataSet() + .set(Parser.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create())) + val parser = Parser.builder(options).build() + val renderer = HtmlRenderer.builder(options).build() + val process = fun(it: String) = renderer.render(parser.parse(it)) + + issue.description = process(issue.description) + for (comment in comments) { + comment.comment = process(comment.comment) + } + } +} + +class IssueEditView( + val issue: Issue, + val versions: List, + val components: List, + val users: List, + val project: Project, // TODO: allow null values to create issues from the IssuesServlet + val version: Version? = null, + val component: Component? = null +) : EditView() { + + val versionsUpcoming: List + val versionsRecent: List + + val issueStatus = IssueStatus.values() + val issueCategory = IssueCategory.values() + + init { + val recent = mutableListOf() + val upcoming = mutableListOf() + recent.addAll(issue.affectedVersions) + upcoming.addAll(issue.resolvedVersions) + for (v in versions) { + if (v.status.isReleased) { + if (v.status != VersionStatus.Deprecated) recent.add(v) + } else { + upcoming.add(v) + } + } + versionsRecent = recent + versionsUpcoming = upcoming + } +} + diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/LanguageView.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/LanguageView.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import java.util.* + +class LanguageView( + val languages: List, + val currentLanguage: Locale, + val browserLanguage: Locale +) : View() \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/NavMenus.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,128 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import de.uapcore.lightpit.entities.Component +import de.uapcore.lightpit.entities.Project +import de.uapcore.lightpit.entities.Version + +class NavMenuEntry( + val level: Int, + val caption: String, + val href: String, + val title: String = "", + val active: Boolean = false, + val resolveCaption: Boolean = false, + val iconColor: String? = null +) { + val iconUseCssClass = iconColor != null && !iconColor.startsWith("#") +} + +class NavMenu(val entries: List) + +fun projectNavMenu( + projects: List, + versions: List = emptyList(), + components: List = emptyList(), + selectedProject: Project? = null, + selectedVersion: Version? = null, + selectedComponent: Component? = null +) = NavMenu( + sequence { + val cnode = selectedComponent?.node ?: "-" + val vnode = selectedVersion?.node ?: "-" + for (project in projects) { + val active = project == selectedProject + yield( + NavMenuEntry( + level = 0, + caption = project.name, + href = "projects/${project.node}", + active = active + ) + ) + if (active) { + yield( + NavMenuEntry( + level = 1, + caption = "navmenu.versions", + resolveCaption = true, + href = "projects/${project.node}/versions/" + ) + ) + yield( + NavMenuEntry( + level = 2, + caption = "navmenu.all", + resolveCaption = true, + href = "projects/${project.node}/issues/-/${cnode}/", + iconColor = "#000000" + ) + ) + for (version in versions) { + yield( + NavMenuEntry( + level = 2, + caption = version.name, + title = "version.status.${version.status}", + href = "projects/${project.node}/issues/${version.node}/${cnode}/", + iconColor = "version-${version.status}", + active = version == selectedVersion + ) + ) + } + yield( + NavMenuEntry( + level = 1, + caption = "navmenu.components", + resolveCaption = true, + href = "projects/${project.node}/components/" + ) + ) + yield( + NavMenuEntry( + level = 2, + caption = "navmenu.all", + resolveCaption = true, + href = "projects/${project.node}/issues/${vnode}/-/", + iconColor = "#000000" + ) + ) + for (component in components) { + yield( + NavMenuEntry( + level = 2, + caption = component.name, + href = "projects/${project.node}/issues/${vnode}/${component.node}/", + iconColor = "${component.color}", + active = component == selectedComponent + ) + ) + } + } + } + }.toList() +) diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Projects.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import de.uapcore.lightpit.entities.* +import de.uapcore.lightpit.types.VersionStatus + +class ProjectInfo( + val project: Project, + /** + * List of versions, sorted by status descending. + */ + var versions: List, + var components: List, + var issueSummary: IssueSummary +) { + val latestVersion = versions.firstOrNull { it.status == VersionStatus.Released } + val nextVersion = versions.findLast { !it.status.isReleased } +} + +class ProjectsView( + val projects: List +) : View() + +class ProjectDetails( + val projectInfo: ProjectInfo, + val issues: List, + val version: Version? = null, + val component: Component? = null +) : View() { + val issueSummary = IssueSummary() + val versionInfo: VersionInfo? + + init { + issues.forEach(issueSummary::add) + versionInfo = version?.let { VersionInfo(it, issues) } + } +} + +class ProjectEditView( + val project: Project, + val users: List +) : EditView() diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/Users.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Users.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import de.uapcore.lightpit.entities.User + +class UsersView( + val users: List +) : View() + +class UserEditView( + val user: User +) : EditView() diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Versions.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +import de.uapcore.lightpit.entities.Issue +import de.uapcore.lightpit.entities.Version +import de.uapcore.lightpit.types.VersionStatus + +class VersionInfo( + val version: Version, + val issues: List +) { + val reportedTotal = IssueSummary() + val resolvedTotal = IssueSummary() + val reported: List + val resolved: List + + init { + val reported = mutableListOf() + val resolved = mutableListOf() + for (issue in issues) { + if (issue.affectedVersions.contains(version)) { + reportedTotal.add(issue) + reported.add(issue) + } + if (issue.resolvedVersions.contains(version)) { + resolvedTotal.add(issue) + resolved.add(issue) + } + } + this.reported = reported + this.resolved = resolved + } +} + +class VersionSummary( + val version: Version +) { + val reportedTotal = IssueSummary() + val resolvedTotal = IssueSummary() +} + +class VersionsView( + val projectInfo: ProjectInfo, + val versionInfos: List +) : View() + +class VersionEditView( + val projectInfo: ProjectInfo, + val version: Version +) : EditView() { + val versionStatus = VersionStatus.values() +} diff -r 61669abf277f -r e8eecee6aadf src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt Fri Apr 02 11:59:14 2021 +0200 @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.uapcore.lightpit.viewmodel + +abstract class View +abstract class EditView : View() { + var errorText: String? = null +} diff -r 61669abf277f -r e8eecee6aadf src/main/resources/localization/strings.properties --- a/src/main/resources/localization/strings.properties Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/resources/localization/strings.properties Fri Apr 02 11:59:14 2021 +0200 @@ -33,6 +33,7 @@ button.language.submit = Switch language button.okay=OK button.project.create=New Project +button.project.edit=Edit Project button.user.create=Add Developer button.version.create=New Version button.version.edit=Edit Version @@ -80,7 +81,6 @@ issue.status=Status issue.subject=Subject issue.updated=Updated -issue.without-version=No Assigned Version issues.active=In Progress issues.done=Done issues.open=Open @@ -94,7 +94,6 @@ menu.users=Developer navmenu.all=all navmenu.components=Components -navmenu.unassigned=unassigned navmenu.versions=Versions no-projects=Welcome to LightPIT. Start off by creating a new project! no-users=No developers have been configured yet. @@ -118,6 +117,7 @@ user.lastname=Last Name user.mail=E-Mail username=User Name +validation.username.unique=Username is already taken. version.latest=Latest Version version.next=Next Version version.status.Deprecated=Deprecated @@ -126,4 +126,4 @@ version.status.Released=Released version.status.Unreleased=Unreleased version.status=Status -version=Version +version=Version \ No newline at end of file diff -r 61669abf277f -r e8eecee6aadf src/main/resources/localization/strings_de.properties --- a/src/main/resources/localization/strings_de.properties Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/resources/localization/strings_de.properties Fri Apr 02 11:59:14 2021 +0200 @@ -33,6 +33,7 @@ button.language.submit = Sprache ausw\u00e4hlen button.okay=OK button.project.create=Neues Projekt +button.project.edit=Projekt Bearbeiten button.user.create=Neuer Entwickler button.version.create=Neue Version button.version.edit=Version Bearbeiten @@ -80,7 +81,6 @@ issue.status=Status issue.subject=Thema issue.updated=Aktualisiert -issue.without-version=Keine Version zugeordnet issues.active=In Arbeit issues.done=Erledigt issues.open=Offen @@ -94,7 +94,6 @@ menu.users=Entwickler navmenu.all=Alle navmenu.components=Komponenten -navmenu.unassigned=Nicht Zugewiesen navmenu.versions=Versionen no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes! no-users=Bislang wurden keine Entwickler hinterlegt. @@ -118,6 +117,7 @@ user.lastname=Nachname user.mail=E-Mail username=Benutzername +validation.username.unique=Der Benutzername wird bereits verwendet. version.latest=Neuste Version version.next=N\u00e4chste Version version.status.Deprecated=Veraltet diff -r 61669abf277f -r e8eecee6aadf src/main/webapp/WEB-INF/jsp/component-form.jsp --- a/src/main/webapp/WEB-INF/jsp/component-form.jsp Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/webapp/WEB-INF/jsp/component-form.jsp Fri Apr 02 11:59:14 2021 +0200 @@ -32,7 +32,7 @@ -

+ @@ -43,7 +43,7 @@ diff -r 61669abf277f -r e8eecee6aadf src/main/webapp/WEB-INF/jsp/components.jsp --- a/src/main/webapp/WEB-INF/jsp/components.jsp Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/webapp/WEB-INF/jsp/components.jsp Fri Apr 02 11:59:14 2021 +0200 @@ -35,8 +35,8 @@ <%@include file="../jspf/project-header.jspf"%>
- - + +

@@ -75,7 +75,7 @@ diff -r 61669abf277f -r e8eecee6aadf src/main/webapp/WEB-INF/jsp/error.jsp --- a/src/main/webapp/WEB-INF/jsp/error.jsp Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/webapp/WEB-INF/jsp/error.jsp Fri Apr 02 11:59:14 2021 +0200 @@ -26,14 +26,13 @@ --%> <%@page pageEncoding="UTF-8" %> <%@page import="de.uapcore.lightpit.Constants" %> -<%@page import="de.uapcore.lightpit.modules.ErrorModule" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> - +

diff -r 61669abf277f -r e8eecee6aadf src/main/webapp/WEB-INF/jsp/issue-form.jsp --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Sat Jan 23 14:47:59 2021 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Fri Apr 02 11:59:14 2021 +0200 @@ -29,10 +29,15 @@ <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - -<%-- TODO: change to ./issues/commit --%> - + + + + + + + +
- +
- +
@@ -49,12 +54,12 @@
- + - + -