major refactoring of DAO architecture - also fixes #114

Mon, 21 Dec 2020 18:29:34 +0100

author
Mike Becker <universe@uap-core.de>
date
Mon, 21 Dec 2020 18:29:34 +0100
changeset 167
3f30adba1c63
parent 166
6eede6088d41
child 168
1c3694ae224c
child 169
672982f54677

major refactoring of DAO architecture - also fixes #114

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/Functions.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/UsersModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/Logging.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractChildEntityDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractComponentDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractEntityDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractIssueDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractProjectDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractUserDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/AbstractVersionDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DaoProvider.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/Extensions.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGComponentDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGDaoProvider.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGIssueDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGProjectDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGUserDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGVersionDao.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Component.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/IssueComment.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/IssueSummary.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Project.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/User.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/Version.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/filter/Filter.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/filter/IssueFilter.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/IssueCategory.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/IssueStatus.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/IssueStatusPhase.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/VersionStatus.kt file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-navmenu.jsp file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Mon Dec 21 18:29:34 2020 +0100
@@ -28,8 +28,8 @@
  */
 package de.uapcore.lightpit;
 
-import de.uapcore.lightpit.dao.DaoProvider;
-import de.uapcore.lightpit.dao.postgres.PGDaoProvider;
+import de.uapcore.lightpit.dao.DataAccessObject;
+import de.uapcore.lightpit.dao.PostgresDataAccessObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -56,26 +56,6 @@
 
     private static final String SITE_JSP = jspPath("site");
 
-
-    @FunctionalInterface
-    protected interface SQLFindFunction<K, T> {
-        T apply(K key) throws SQLException;
-
-        default <V> SQLFindFunction<V, T> compose(Function<? super V, ? extends K> before) throws SQLException {
-            Objects.requireNonNull(before);
-            return (v) -> this.apply(before.apply(v));
-        }
-
-        default <V> SQLFindFunction<K, V> andThen(Function<? super T, ? extends V> after) throws SQLException {
-            Objects.requireNonNull(after);
-            return (t) -> after.apply(this.apply(t));
-        }
-
-        static <K> Function<K, K> identity() {
-            return (t) -> t;
-        }
-    }
-
     /**
      * Invocation mapping gathered from the {@link RequestMapping} annotations.
      * <p>
@@ -101,15 +81,15 @@
      * @param connection the SQL connection
      * @return a set of data access objects
      */
-    private DaoProvider createDataAccessObjects(Connection connection) throws SQLException {
+    private DataAccessObject createDataAccessObjects(Connection connection) {
         final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
         if (df.getDialect() == DataSourceProvider.Dialect.Postgres) {
-            return new PGDaoProvider(connection);
+            return new PostgresDataAccessObject(connection);
         }
         throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug.");
     }
 
-    private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException {
+    private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException {
         final var pathPattern = mapping.getKey();
         final var method = mapping.getValue();
         try {
@@ -122,7 +102,7 @@
                 } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
                     paramValues[i] = resp;
                 }
-                if (paramTypes[i].isAssignableFrom(DaoProvider.class)) {
+                if (paramTypes[i].isAssignableFrom(DataAccessObject.class)) {
                     paramValues[i] = dao;
                 }
                 if (paramTypes[i].isAssignableFrom(PathParameters.class)) {
@@ -179,9 +159,9 @@
                     boolean paramsInjectible = true;
                     for (var param : method.getParameterTypes()) {
                         paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
-                                || HttpServletResponse.class.isAssignableFrom(param)
-                                || PathParameters.class.isAssignableFrom(param)
-                                || DaoProvider.class.isAssignableFrom(param);
+                                            || HttpServletResponse.class.isAssignableFrom(param)
+                                            || PathParameters.class.isAssignableFrom(param)
+                                            || DataAccessObject.class.isAssignableFrom(param);
                     }
                     if (paramsInjectible) {
                         try {
@@ -360,7 +340,7 @@
      * @return the retrieved entity or an empty optional if there is no such entity or the request parameter was missing
      * @throws SQLException if the find function throws an exception
      */
-    protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, SQLFindFunction<? super T, ? extends R> find) throws SQLException {
+    protected <T, R> Optional<R> findByParameter(HttpServletRequest req, Class<T> clazz, String name, Function<? super T, ? extends R> find) {
         final var param = getParameter(req, clazz, name);
         if (param.isPresent()) {
             return Optional.ofNullable(find.apply(param.get()));
--- a/src/main/java/de/uapcore/lightpit/dao/Functions.java	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2018 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 java.sql.*;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Function;
-
-/**
- * Some DAO utilities.
- */
-public final class Functions {
-
-    public static String getSafeString(ResultSet rs, String column) throws SQLException {
-        return Optional.ofNullable(rs.getString(column)).orElse("");
-    }
-
-    public static void setStringOrNull(PreparedStatement stmt, int index, String str) throws SQLException {
-        if (str == null || str.isBlank()) {
-            stmt.setNull(index, Types.VARCHAR);
-        } else {
-            stmt.setString(index, str);
-        }
-    }
-
-    public static void setDateOrNull(PreparedStatement stmt, int index, Date date) throws SQLException {
-        if (date == null) {
-            stmt.setNull(index, Types.DATE);
-        } else {
-            stmt.setDate(index, date);
-        }
-    }
-
-    public static <T> void setForeignKeyOrNull(PreparedStatement stmt, int index, T instance, Function<? super T, Integer> keyGetter) throws SQLException {
-        Integer key = Optional.ofNullable(instance).map(keyGetter).orElse(null);
-        if (key == null) {
-            stmt.setNull(index, Types.INTEGER);
-        } else {
-            stmt.setInt(index, key);
-        }
-    }
-
-    @FunctionalInterface
-    public interface ResultSetMapper<T> {
-        T apply(ResultSet rs) throws SQLException;
-    }
-
-    public static <T> List<T> list(PreparedStatement stmt, ResultSetMapper<T> mapper) throws SQLException {
-        List<T> results = new ArrayList<>();
-        try (var result = stmt.executeQuery()) {
-            while (result.next()) {
-                final var project = mapper.apply(result);
-                results.add(project);
-            }
-        }
-        return results;
-    }
-
-    public static <T> T find(PreparedStatement stmt, ResultSetMapper<T> mapper) throws SQLException {
-        try (var result = stmt.executeQuery()) {
-            if (result.next()) {
-                final var ent = mapper.apply(result);
-                return ent;
-            } else {
-                return null;
-            }
-        }
-    }
-
-    private Functions() {
-
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Mon Dec 21 18:29:34 2020 +0100
@@ -30,8 +30,15 @@
 
 
 import de.uapcore.lightpit.*;
-import de.uapcore.lightpit.dao.DaoProvider;
+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;
@@ -45,7 +52,6 @@
 import java.io.IOException;
 import java.sql.Date;
 import java.sql.SQLException;
-import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -72,25 +78,21 @@
         }
     }
 
-    private void populate(ProjectView viewModel, PathParameters pathParameters, DaoProvider dao) {
-        final var projectDao = dao.getProjectDao();
-        final var versionDao = dao.getVersionDao();
-        final var componentDao = dao.getComponentDao();
-
-        projectDao.list().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add);
+    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 = projectDao.findByNode(pathParameters.get("project"));
+        final var project = dao.findProjectByNode(pathParameters.get("project"));
         if (project == null)
             return;
 
         final var info = new ProjectInfo(project);
-        info.setVersions(versionDao.list(project));
-        info.setComponents(componentDao.list(project));
-        info.setIssueSummary(projectDao.getIssueSummary(project));
+        info.setVersions(dao.listVersions(project));
+        info.setComponents(dao.listComponents(project));
+        info.setIssueSummary(dao.collectIssueSummary(project));
         viewModel.setProjectInfo(info);
 
         // Select Version
@@ -101,7 +103,7 @@
             } else if ("all-versions".equals(versionNode)) {
                 viewModel.setVersionFilter(ProjectView.ALL_VERSIONS);
             } else {
-                viewModel.setVersionFilter(versionDao.findByNode(project, versionNode));
+                viewModel.setVersionFilter(dao.findVersionByNode(project, versionNode));
             }
         }
 
@@ -113,7 +115,7 @@
             } else if ("all-components".equals(componentNode)) {
                 viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS);
             } else {
-                viewModel.setComponentFilter(componentDao.findByNode(project, componentNode));
+                viewModel.setComponentFilter(dao.findComponentByNode(project, componentNode));
             }
         }
     }
@@ -137,28 +139,25 @@
     }
 
     @RequestMapping(method = HttpMethod.GET)
-    public void index(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
+    public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException {
         final var viewModel = new ProjectView();
         populate(viewModel, null, dao);
 
-        final var projectDao = dao.getProjectDao();
-        final var versionDao = dao.getVersionDao();
-
         for (var info : viewModel.getProjectList()) {
-            info.setVersions(versionDao.list(info.getProject()));
-            info.setIssueSummary(projectDao.getIssueSummary(info.getProject()));
+            info.setVersions(dao.listVersions(info.getProject()));
+            info.setIssueSummary(dao.collectIssueSummary(info.getProject()));
         }
 
         forwardView(req, resp, viewModel, "projects");
     }
 
-    private void configureProjectEditor(ProjectEditView viewModel, Project project, DaoProvider dao) throws SQLException {
+    private void configureProjectEditor(ProjectEditView viewModel, Project project, DataAccessObject dao) {
         viewModel.setProject(project);
-        viewModel.setUsers(dao.getUserDao().list());
+        viewModel.setUsers(dao.listUsers());
     }
 
     @RequestMapping(requestPath = "$project/edit", method = HttpMethod.GET)
-    public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws IOException, SQLException, ServletException {
         final var viewModel = new ProjectEditView();
         populate(viewModel, pathParams, dao);
 
@@ -172,7 +171,7 @@
     }
 
     @RequestMapping(requestPath = "create", method = HttpMethod.GET)
-    public void create(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
+    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);
@@ -180,7 +179,7 @@
     }
 
     @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
-    public void commit(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
+    public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
 
         try {
             final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
@@ -195,12 +194,10 @@
                     ownerId -> ownerId >= 0 ? new User(ownerId) : null
             ).ifPresent(project::setOwner);
 
-            final var projectDao = dao.getProjectDao();
             if (project.getId() > 0) {
-                // TODO: unused return value
-                projectDao.update(project);
+                dao.updateProject(project);
             } else {
-                projectDao.save(project);
+                dao.insertProject(project);
             }
 
             setRedirectLocation(req, "./projects/");
@@ -215,7 +212,7 @@
     }
 
     @RequestMapping(requestPath = "$project/$component/$version/issues/", method = HttpMethod.GET)
-    public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DaoProvider dao) throws SQLException, IOException, ServletException {
+    public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws SQLException, IOException, ServletException {
         final var viewModel = new ProjectDetailsView();
         populate(viewModel, pathParams, dao);
 
@@ -228,36 +225,64 @@
         final var version = viewModel.getVersionFilter();
         final var component = viewModel.getComponentFilter();
 
-        final var issueDao = dao.getIssueDao();
+        // TODO: use new IssueFilter class for the ViewModel
 
-        final List<Issue> issues;
+        final var projectFilter = new SpecificFilter<>(project);
+        final IssueFilter filter;
         if (version.equals(ProjectView.NO_VERSION)) {
             if (component.equals(ProjectView.ALL_COMPONENTS)) {
-                issues = issueDao.list(project, (Version) null);
+                filter = new IssueFilter(projectFilter,
+                        new NoneFilter<>(),
+                        new AllFilter<>()
+                );
             } else if (component.equals(ProjectView.NO_COMPONENT)) {
-                issues = issueDao.list(project, null, null);
+                filter = new IssueFilter(projectFilter,
+                        new NoneFilter<>(),
+                        new NoneFilter<>()
+                );
             } else {
-                issues = issueDao.list(project, component, null);
+                filter = new IssueFilter(projectFilter,
+                        new NoneFilter<>(),
+                        new SpecificFilter<>(component)
+                );
             }
         } else if (version.equals(ProjectView.ALL_VERSIONS)) {
             if (component.equals(ProjectView.ALL_COMPONENTS)) {
-                issues = issueDao.list(project);
+                filter = new IssueFilter(projectFilter,
+                        new AllFilter<>(),
+                        new AllFilter<>()
+                );
             } else if (component.equals(ProjectView.NO_COMPONENT)) {
-                issues = issueDao.list(project, (Component)null);
+                filter = new IssueFilter(projectFilter,
+                        new AllFilter<>(),
+                        new NoneFilter<>()
+                );
             } else {
-                issues = issueDao.list(project, component);
+                filter = new IssueFilter(projectFilter,
+                        new AllFilter<>(),
+                        new SpecificFilter<>(component)
+                );
             }
         } else {
             if (component.equals(ProjectView.ALL_COMPONENTS)) {
-                issues = issueDao.list(project, version);
+                filter = new IssueFilter(projectFilter,
+                        new SpecificFilter<>(version),
+                        new AllFilter<>()
+                );
             } else if (component.equals(ProjectView.NO_COMPONENT)) {
-                issues = issueDao.list(project, null, version);
+                filter = new IssueFilter(projectFilter,
+                        new SpecificFilter<>(version),
+                        new NoneFilter<>()
+                );
             } else {
-                issues = issueDao.list(project, component, version);
+                filter = new IssueFilter(projectFilter,
+                        new SpecificFilter<>(version),
+                        new SpecificFilter<>(component)
+                );
             }
         }
 
-        for (var issue : issues) issueDao.joinVersionInformation(issue);
+        final var issues = dao.listIssues(filter);
         issues.sort(new IssueSorter(
                 new IssueSorter.Criteria(IssueSorter.Field.DONE, true),
                 new IssueSorter.Criteria(IssueSorter.Field.ETA, true),
@@ -273,7 +298,7 @@
     }
 
     @RequestMapping(requestPath = "$project/versions/", method = HttpMethod.GET)
-    public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
         final var viewModel = new VersionsView();
         populate(viewModel, pathParameters, dao);
 
@@ -283,16 +308,20 @@
             return;
         }
 
-        final var issueDao = dao.getIssueDao();
-        final var issues = issueDao.list(projectInfo.getProject());
-        for (var issue : issues) issueDao.joinVersionInformation(issue);
+        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, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
         final var viewModel = new VersionEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -307,7 +336,7 @@
     }
 
     @RequestMapping(requestPath = "$project/create-version", method = HttpMethod.GET)
-    public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
         final var viewModel = new VersionEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -316,22 +345,22 @@
             return;
         }
 
-        viewModel.setVersion(new Version(-1));
+        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, DaoProvider dao) throws IOException, ServletException {
+    public void commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
 
         try {
-            final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
+            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());
+            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);
@@ -340,12 +369,10 @@
             getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
             version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
 
-            final var versionDao = dao.getVersionDao();
             if (version.getId() > 0) {
-                // TODO: use return value
-                versionDao.update(version);
+                dao.updateVersion(version);
             } else {
-                versionDao.save(version, project);
+                dao.insertVersion(version);
             }
 
             setRedirectLocation(req, "./projects/" + project.getNode() + "/versions/");
@@ -359,7 +386,7 @@
     }
 
     @RequestMapping(requestPath = "$project/components/", method = HttpMethod.GET)
-    public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
         final var viewModel = new ComponentsView();
         populate(viewModel, pathParameters, dao);
 
@@ -369,15 +396,20 @@
             return;
         }
 
-        final var issueDao = dao.getIssueDao();
-        final var issues = issueDao.list(projectInfo.getProject());
+        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, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
         final var viewModel = new ComponentEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -387,13 +419,13 @@
         }
 
         viewModel.setComponent(viewModel.getComponentFilter());
-        viewModel.setUsers(dao.getUserDao().list());
+        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, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
         final var viewModel = new ComponentEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -402,23 +434,23 @@
             return;
         }
 
-        viewModel.setComponent(new Component(-1));
-        viewModel.setUsers(dao.getUserDao().list());
+        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, DaoProvider dao) throws IOException, ServletException {
+    public void commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
 
         try {
-            final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
+            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());
+            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);
@@ -431,12 +463,10 @@
             ).ifPresent(component::setLead);
             getParameter(req, String.class, "description").ifPresent(component::setDescription);
 
-            final var componentDao = dao.getComponentDao();
             if (component.getId() > 0) {
-                // TODO: use return value
-                componentDao.update(component);
+                dao.updateComponent(component);
             } else {
-                componentDao.save(component, project);
+                dao.insertComponent(component);
             }
 
             setRedirectLocation(req, "./projects/" + project.getNode() + "/components/");
@@ -449,17 +479,17 @@
         }
     }
 
-    private void configureIssueEditor(IssueEditView viewModel, Issue issue, DaoProvider dao) throws SQLException {
+    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.getUserDao().list());
-        viewModel.setComponents(dao.getComponentDao().list(project));
+        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, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void viewIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException {
         final var viewModel = new IssueDetailView();
         populate(viewModel, pathParameters, dao);
 
@@ -469,16 +499,14 @@
             return;
         }
 
-        final var issueDao = dao.getIssueDao();
-        final var issue = issueDao.find(parseIntOrZero(pathParameters.get("issue")));
+        final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue")));
         if (issue == null) {
             resp.sendError(HttpServletResponse.SC_NOT_FOUND);
             return;
         }
 
-        issueDao.joinVersionInformation(issue);
         viewModel.setIssue(issue);
-        viewModel.setComments(issueDao.listComments(issue));
+        viewModel.setComments(dao.listComments(issue));
 
         viewModel.processMarkdown();
 
@@ -487,7 +515,7 @@
 
     // 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, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void editIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
         final var viewModel = new IssueEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -497,21 +525,19 @@
             return;
         }
 
-        final var issueDao = dao.getIssueDao();
-        final var issue = issueDao.find(parseIntOrZero(pathParameters.get("issue")));
+        final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue")));
         if (issue == null) {
             resp.sendError(HttpServletResponse.SC_NOT_FOUND);
             return;
         }
 
-        issueDao.joinVersionInformation(issue);
         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, DaoProvider dao) throws IOException, SQLException, ServletException {
+    public void createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException {
         final var viewModel = new IssueEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -521,7 +547,8 @@
             return;
         }
 
-        final var issue = new Issue(-1);
+        // TODO: fix #38 - automatically select component (and version)
+        final var issue = new Issue(-1, projectInfo.getProject(), null);
         issue.setProject(projectInfo.getProject());
         configureIssueEditor(viewModel, issue, dao);
 
@@ -529,27 +556,25 @@
     }
 
     @RequestMapping(requestPath = "commit-issue", method = HttpMethod.POST)
-    public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
+    public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException {
         try {
-            final var issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
-            final var componentId = getParameter(req, Integer.class, "component");
-            final Component component;
-            if (componentId.isPresent()) {
-                component = dao.getComponentDao().find(componentId.get());
-            } else {
-                component = null;
-            }
-            final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
+            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;
             }
-            issue.setProject(project);
+            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());
-            issue.setComponent(component);
             getParameter(req, Integer.class, "assignee").map(userid -> {
                 if (userid >= 0) {
                     return new User(userid);
@@ -566,23 +591,23 @@
             getParameter(req, Integer[].class, "affected")
                     .map(Stream::of)
                     .map(stream ->
-                            stream.map(Version::new).collect(Collectors.toList())
+                            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(Version::new).collect(Collectors.toList())
+                            stream.map(id -> new Version(id, project.getId()))
+                                    .collect(Collectors.toList())
                     ).ifPresent(issue::setResolvedVersions);
 
-            final var issueDao = dao.getIssueDao();
             if (issue.getId() > 0) {
-                // TODO: use return value
-                issueDao.update(issue);
+                dao.updateIssue(issue);
             } else {
-                issueDao.save(issue, project);
+                dao.insertIssue(issue);
             }
 
-            // TODO: fix redirect location
+            // TODO: implement #110
             setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/view");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
@@ -594,19 +619,19 @@
     }
 
     @RequestMapping(requestPath = "commit-issue-comment", method = HttpMethod.POST)
-    public void commentIssue(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
+    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.getIssueDao().find(issueIdParam.get());
+        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));
+            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()) {
@@ -615,12 +640,11 @@
 
             LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId());
             if (req.getRemoteUser() != null) {
-                Optional.ofNullable(dao.getUserDao().findByUsername(req.getRemoteUser())).ifPresent(issueComment::setAuthor);
+                Optional.ofNullable(dao.findUserByName(req.getRemoteUser())).ifPresent(issueComment::setAuthor);
             }
 
-            dao.getIssueDao().saveComment(issue, issueComment);
+            dao.insertComment(issueComment);
 
-            // TODO: fix redirect location
             setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/view");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
--- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Mon Dec 21 18:29:34 2020 +0100
@@ -32,7 +32,7 @@
 import de.uapcore.lightpit.Constants;
 import de.uapcore.lightpit.HttpMethod;
 import de.uapcore.lightpit.RequestMapping;
-import de.uapcore.lightpit.dao.DaoProvider;
+import de.uapcore.lightpit.dao.DataAccessObject;
 import de.uapcore.lightpit.entities.User;
 import de.uapcore.lightpit.viewmodel.UsersEditView;
 import de.uapcore.lightpit.viewmodel.UsersView;
@@ -61,11 +61,9 @@
     }
 
     @RequestMapping(method = HttpMethod.GET)
-    public void index(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
-        final var userDao = dao.getUserDao();
-
+    public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException {
         final var viewModel = new UsersView();
-        viewModel.setUsers(userDao.list());
+        viewModel.setUsers(dao.listUsers());
         setViewModel(req, viewModel);
         setContentPage(req, "users");
 
@@ -73,11 +71,10 @@
     }
 
     @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
-    public void edit(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
+    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.getUserDao()::find).orElse(new User(-1)));
+        viewModel.setUser(findByParameter(req, Integer.class, "id", dao::findUser).orElse(new User(-1)));
 
         setViewModel(req, viewModel);
         setContentPage(req, "user-form");
@@ -86,7 +83,7 @@
     }
 
     @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
-    public void commit(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws ServletException, IOException {
+    public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException {
 
         User user = new User(-1);
         try {
@@ -96,12 +93,10 @@
             getParameter(req, String.class, "lastname").ifPresent(user::setLastname);
             getParameter(req, String.class, "mail").ifPresent(user::setMail);
 
-            final var userDao = dao.getUserDao();
             if (user.getId() > 0) {
-                // TODO: unused return value
-                userDao.update(user);
+                dao.updateUser(user);
             } else {
-                userDao.save(user);
+                dao.insertUser(user);
             }
 
             setRedirectLocation(req, "./teams/");
--- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Mon Dec 21 18:29:34 2020 +0100
@@ -1,6 +1,12 @@
 package de.uapcore.lightpit.viewmodel;
 
-import de.uapcore.lightpit.entities.*;
+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.*;
 
--- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Mon Dec 21 18:29:34 2020 +0100
@@ -12,10 +12,12 @@
     public static final int SELECTED_PAGE_VERSIONS = 1;
     public static final int SELECTED_PAGE_COMPONENTS = 2;
 
-    public static final Version ALL_VERSIONS = new Version(0);
-    public static final Version NO_VERSION = new Version(-1);
-    public static final Component ALL_COMPONENTS = new Component(0);
-    public static final Component NO_COMPONENT = new Component(-1);
+    // 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");
--- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Mon Dec 21 18:29:34 2020 +0100
@@ -1,7 +1,7 @@
 package de.uapcore.lightpit.viewmodel;
 
 import de.uapcore.lightpit.entities.Version;
-import de.uapcore.lightpit.entities.VersionStatus;
+import de.uapcore.lightpit.types.VersionStatus;
 
 public class VersionEditView extends ProjectView {
     private Version version;
--- a/src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/util/IssueSorter.java	Mon Dec 21 18:29:34 2020 +0100
@@ -1,7 +1,7 @@
 package de.uapcore.lightpit.viewmodel.util;
 
 import de.uapcore.lightpit.entities.Issue;
-import de.uapcore.lightpit.entities.IssueStatusPhase;
+import de.uapcore.lightpit.types.IssueStatusPhase;
 
 import java.util.Arrays;
 import java.util.Comparator;
--- a/src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/DataSourceProvider.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -128,7 +128,7 @@
         val sc = sce!!.servletContext
 
         val dbSchema = sc.getInitParameter(Constants.CTX_ATTR_DB_SCHEMA) ?: DB_DEFAULT_SCHEMA
-        sc.getInitParameter(Constants.CTX_ATTR_DB_DIALECT)?.let {dbDialect ->
+        sc.getInitParameter(Constants.CTX_ATTR_DB_DIALECT)?.let { dbDialect ->
             try {
                 dialect = Dialect.valueOf(dbDialect)
             } catch (ex: IllegalArgumentException) {
--- a/src/main/kotlin/de/uapcore/lightpit/Logging.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/Logging.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -29,4 +29,5 @@
 import org.slf4j.LoggerFactory
 
 interface LoggingTrait
-inline fun <reified T : LoggingTrait> T.logger(): Logger = LoggerFactory.getLogger(T::class.java);
+
+inline fun <reified T : LoggingTrait> T.logger(): Logger = LoggerFactory.getLogger(T::class.java)
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractChildEntityDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/*
- * Copyright 2020 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
-
-
-abstract class AbstractChildEntityDao<T, P> : AbstractDao<T>() {
-
-    /**
-     * Lists all entities being a child of the specified parent.
-     * @param parent the parent
-     * @return the list of child instances
-     */
-    abstract fun list(parent: P): List<T>
-
-    /**
-     * Finds an entity by its integer ID.
-     * It is not guaranteed that referenced entities are automatically joined.
-     *
-     * @param id the id
-     * @return the entity or null if there is no such entity
-     */
-    abstract fun find(id: Int): T?
-
-    /**
-     * Inserts an instance into database.
-     * It is not guaranteed that generated fields will be updated in the instance.
-     *
-     * @param instance the instance to insert
-     * @param parent a reference to the parent
-     */
-    abstract fun save(instance: T, parent: P)
-
-    /**
-     * Updates an instance in the database.
-     *
-     * @param instance the instance to update
-     * @return true if an instance has been updated, false if the instance is not present in database
-     */
-    abstract fun update(instance: T): Boolean
-}
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractComponentDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/*
- * Copyright 2020 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.entities.Component
-import de.uapcore.lightpit.entities.Project
-
-abstract class AbstractComponentDao : AbstractChildEntityDao<Component, Project>() {
-    abstract fun findByNode(parent: Project, node: String): Component?
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/*
- * Copyright 2020 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 java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.sql.Types
-
-abstract class AbstractDao<T> {
-
-    abstract fun mapResult(rs: ResultSet): T
-
-    protected fun list(stmt: PreparedStatement): List<T> {
-        return sequence {
-            stmt.executeQuery().use { result ->
-                while (result.next()) yield(mapResult(result))
-            }
-        }.toList()
-    }
-
-    protected fun find(stmt: PreparedStatement): T? {
-        stmt.executeQuery().use { result ->
-            return if (result.next()) {
-                mapResult(result)
-            } else {
-                null
-            }
-        }
-    }
-
-    // TODO: create PreparedStatement abstraction that provides some features
-
-    // TODO: remove the following legacy code helper function
-    protected fun <T> setForeignKeyOrNull(stmt: PreparedStatement, index: Int, instance: T?, keyGetter: (obj: T) -> Int) {
-        if (instance == null) {
-            stmt.setNull(index, Types.INTEGER)
-        } else {
-            stmt.setInt(index, keyGetter(instance))
-        }
-    }
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractEntityDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/*
- * Copyright 2020 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
-
-abstract class AbstractEntityDao<T> : AbstractDao<T>() {
-
-    /**
-     * Lists all entities.
-     * @return a list of all entities
-     */
-    abstract fun list(): List<T>
-
-    /**
-     * Finds an entity by its integer ID.
-     * It is not guaranteed that referenced entities are automatically joined.
-     *
-     * @param id the id
-     * @return the entity or null if there is no such entity
-     */
-    abstract fun find(id: Int): T?
-
-    /**
-     * Inserts an instance into database.
-     * It is not guaranteed that generated fields will be updated in the instance.
-     *
-     * @param instance the instance to insert
-     */
-    abstract fun save(instance: T)
-
-    /**
-     * Updates an instance in the database.
-     *
-     * @param instance the instance to update
-     * @return true if an instance has been updated, false if the instance is not present in database
-     */
-    abstract fun update(instance: T): Boolean
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractIssueDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/*
- * Copyright 2020 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.entities.*
-import java.sql.SQLException
-
-abstract class AbstractIssueDao : AbstractChildEntityDao<Issue, Project>() {
-
-    /**
-     * Lists all issues that are related to the specified component and version.
-     * If component or version is null, search for issues that are not assigned to any
-     * component or version, respectively.
-     *
-     * @param project the project
-     * @param component the component
-     * @param version the version
-     * @return a list of issues
-     */
-    abstract fun list(project: Project, component: Component?, version: Version?): List<Issue>
-
-    /**
-     * Lists all issues that are related to the specified version.
-     * If the version is null, lists issues that are not assigned to any version.
-     *
-     * @param project the project
-     * @param version the version or null
-     * @return a list of issues
-     */
-    abstract fun list(project: Project, version: Version?): List<Issue>
-
-    /**
-     * Lists all issues that are related to the specified component.
-     * If the component is null, lists issues that are not assigned to a component.
-     *
-     * @param project the project
-     * @param component the component or null
-     * @return a list of issues
-     */
-    abstract fun list(project: Project, component: Component?): List<Issue>
-
-    /**
-     * Lists all comments for a specific issue in chronological order.
-     *
-     * @param issue the issue
-     * @return the list of comments
-     */
-    abstract fun listComments(issue: Issue): List<IssueComment>
-
-    /**
-     * Stores the specified comment in database.
-     * This is an update-or-insert operation.
-     * The "updated" date of the corresponding issue is also updated.
-     *
-     * @param issue the issue to save the comment for
-     * @param comment the comment to save
-     */
-    abstract fun saveComment(issue: Issue, comment: IssueComment)
-
-    /**
-     * Saves an instances to the database.
-     * Implementations of this DAO must guarantee that the generated ID is stored in the instance.
-     *
-     * @param instance the instance to insert
-     * @param parent the parent project
-     * @throws SQLException on any kind of SQL error
-     */
-    abstract override fun save(instance: Issue, parent: Project)
-
-    /**
-     * Retrieves the affected, scheduled and resolved versions for the specified issue.
-     *
-     * @param issue the issue to join the information for
-     */
-    abstract fun joinVersionInformation(issue: Issue)
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractProjectDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-/*
- * Copyright 2020 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.entities.IssueSummary
-import de.uapcore.lightpit.entities.Project
-
-abstract class AbstractProjectDao : AbstractEntityDao<Project>() {
-
-    abstract fun getIssueSummary(project: Project): IssueSummary
-
-    abstract fun findByNode(node: String): Project?
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractUserDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-/*
- * Copyright 2020 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.entities.User
-
-abstract class AbstractUserDao : AbstractEntityDao<User>() {
-    abstract fun findByUsername(username: String): User?
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/AbstractVersionDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/*
- * Copyright 2020 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.entities.Project
-import de.uapcore.lightpit.entities.Version
-
-abstract class AbstractVersionDao : AbstractChildEntityDao<Version, Project>() {
-    abstract fun findByNode(parent: Project, node: String): Version?
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DaoProvider.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 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
-
-interface DaoProvider {
-    val userDao: AbstractUserDao
-    val projectDao: AbstractProjectDao
-    val componentDao: AbstractComponentDao
-    val versionDao: AbstractVersionDao
-    val issueDao: AbstractIssueDao
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 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.entities.*
+import de.uapcore.lightpit.filter.IssueFilter
+
+interface DataAccessObject {
+    fun listUsers(): List<User>
+    fun findUser(id: Int): User?
+    fun findUserByName(username: String): User?
+    fun insertUser(user: User)
+    fun updateUser(user: User)
+
+    fun listVersions(project: Project): List<Version>
+    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<Component>
+    fun findComponent(id: Int): Component?
+    fun findComponentByNode(project: Project, node: String): Component?
+    fun insertComponent(component: Component)
+    fun updateComponent(component: Component)
+
+    fun listProjects(): List<Project>
+    fun findProject(id: Int): Project?
+    fun findProjectByNode(node: String): Project?
+    fun insertProject(project: Project)
+    fun updateProject(project: Project)
+
+    fun collectIssueSummary(project: Project): IssueSummary
+
+    fun listIssues(filter: IssueFilter): List<Issue>
+    fun findIssue(id: Int): Issue?
+    fun insertIssue(issue: Issue)
+    fun updateIssue(issue: Issue)
+
+    fun listComments(issue: Issue): List<IssueComment>
+    fun insertComment(issueComment: IssueComment)
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/Extensions.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 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 java.sql.Date
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+import java.sql.Types
+
+fun PreparedStatement.setStringSafe(idx: Int, str: String) {
+    setString(idx, str)
+}
+
+fun PreparedStatement.setStringOrNull(idx: Int, str: String?) {
+    when (str) {
+        null -> setNull(idx, Types.VARCHAR)
+        else -> setString(idx, str)
+    }
+}
+
+fun PreparedStatement.setIntOrNull(idx: Int, value: Int?) {
+    when (value) {
+        null -> setNull(idx, Types.INTEGER)
+        else -> setInt(idx, value)
+    }
+}
+
+fun PreparedStatement.setDateOrNull(idx: Int, value: Date?) {
+    when (value) {
+        null -> setNull(idx, Types.DATE)
+        else -> setDate(idx, value)
+    }
+}
+
+fun <T : Enum<T>> PreparedStatement.setEnum(idx: Int, e: Enum<T>) {
+    setString(idx, e.name)
+}
+
+inline fun <reified T : Enum<T>> ResultSet.getEnum(col: String): T {
+    return enumValueOf(getString(col))
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,739 @@
+/*
+ * Copyright 2020 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.entities.*
+import de.uapcore.lightpit.filter.*
+import de.uapcore.lightpit.types.WebColor
+import java.sql.Connection
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+class PostgresDataAccessObject(private val connection: Connection) : DataAccessObject {
+
+    //<editor-fold desc="User">
+    private fun selectUserInfo(
+        rs: ResultSet,
+        idColumn: String = "userid",
+        usernameColumn: String = "username",
+        givennameColumn: String = "givenname",
+        lastnameColumn: String = "lastname",
+        mailColumn: String = "mail"
+    ): User? {
+        val idval = rs.getInt(idColumn)
+        return if (rs.wasNull()) null else {
+            User(idval).apply {
+                username = rs.getString(usernameColumn)
+                givenname = rs.getString(givennameColumn)
+                lastname = rs.getString(lastnameColumn)
+                mail = rs.getString(mailColumn)
+            }
+        }
+    }
+
+    private fun selectUsers(stmt: PreparedStatement) = sequence {
+        stmt.executeQuery().use { rs ->
+            while (rs.next()) selectUserInfo(rs)?.let { yield(it) }
+        }
+    }
+
+    //language=SQL
+    private val userQuery = "select userid, username, lastname, givenname, mail from lpit_user"
+
+    private val stmtUsers by lazy {
+        connection.prepareStatement(
+            """${userQuery}
+            where userid > 0
+            order by username
+            """
+        )
+    }
+    private val stmtUserByID by lazy {
+        connection.prepareStatement(
+            """${userQuery}
+            where userid = ?
+            """
+        )
+    }
+    private val stmtUserByName by lazy {
+        connection.prepareStatement(
+            """${userQuery}
+            where lower(username) = lower(?)
+            """
+        )
+    }
+    private val stmtInsertUser by lazy {
+        connection.prepareStatement(
+            "insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)"
+        )
+    }
+    private val stmtUpdateUser by lazy {
+        connection.prepareStatement(
+            "update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?"
+        )
+    }
+
+    override fun listUsers() = selectUsers(stmtUsers).toList()
+    override fun findUser(id: Int): User? {
+        stmtUserByID.setInt(1, id)
+        return selectUsers(stmtUserByID).firstOrNull()
+    }
+
+    override fun findUserByName(username: String): User? {
+        stmtUserByName.setString(1, username)
+        return selectUsers(stmtUserByName).firstOrNull()
+    }
+
+    override fun insertUser(user: User) {
+        with(user) {
+            stmtInsertUser.setStringSafe(1, username)
+            stmtInsertUser.setStringOrNull(2, lastname)
+            stmtInsertUser.setStringOrNull(3, givenname)
+            stmtInsertUser.setStringOrNull(4, mail)
+        }
+        stmtInsertUser.execute()
+    }
+
+    override fun updateUser(user: User) {
+        with(user) {
+            stmtUpdateUser.setStringOrNull(1, lastname)
+            stmtUpdateUser.setStringOrNull(2, givenname)
+            stmtUpdateUser.setStringOrNull(3, mail)
+            stmtUpdateUser.setInt(4, id)
+        }
+        stmtUpdateUser.execute()
+    }
+    //</editor-fold>
+
+    //<editor-fold desc="Version">
+    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")
+                })
+            }
+        }
+    }
+
+    private fun setVersionFields(stmt: PreparedStatement, obj: Version): Int {
+        with(obj) {
+            stmt.setStringSafe(1, name)
+            stmt.setStringSafe(2, node)
+            stmt.setInt(3, ordinal)
+            stmt.setEnum(4, status)
+        }
+        return 5
+    }
+
+    //language=SQL
+    private val versionQuery = "select versionid, project, name, node, ordinal, status from lpit_version"
+
+    private val stmtVersions by lazy {
+        connection.prepareStatement(
+            """${versionQuery}
+            where project = ?
+            order by ordinal desc, lower(name) desc
+            """
+        )
+    }
+    private val stmtVersionByID by lazy {
+        connection.prepareStatement(
+            """${versionQuery}
+            where versionid = ?
+            """
+        )
+    }
+    private val stmtVersionByNode by lazy {
+        connection.prepareStatement(
+            """${versionQuery}
+            where project = ? and node = ?
+            """
+        )
+    }
+    private val stmtInsertVersion by lazy {
+        connection.prepareStatement(
+            """
+            insert into lpit_version (name, node, ordinal, status, project)
+            values (?, ?, ?, ?::version_status, ?)
+            """
+        )
+    }
+    private val stmtUpdateVersion by lazy {
+        connection.prepareStatement(
+            """
+            update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status
+            where versionid = ?
+            """
+        )
+    }
+
+    override fun listVersions(project: Project): List<Version> {
+        stmtVersions.setInt(1, project.id)
+        return selectVersions(stmtVersions).toList()
+    }
+
+    override fun findVersion(id: Int): Version? {
+        stmtVersionByID.setInt(1, id)
+        return selectVersions(stmtVersionByID).firstOrNull()
+    }
+
+    override fun findVersionByNode(project: Project, node: String): Version? {
+        stmtVersionByNode.setInt(1, project.id)
+        stmtVersionByNode.setString(2, node)
+        return selectVersions(stmtVersionByNode).firstOrNull()
+    }
+
+    override fun insertVersion(version: Version) {
+        val col = setVersionFields(stmtInsertVersion, version)
+        stmtInsertVersion.setInt(col, version.projectid)
+        stmtInsertVersion.execute()
+    }
+
+    override fun updateVersion(version: Version) {
+        val col = setVersionFields(stmtUpdateVersion, version)
+        stmtUpdateVersion.setInt(col, version.id)
+        stmtUpdateVersion.execute()
+    }
+    //</editor-fold>
+
+    //<editor-fold desc="Component">
+    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)
+                })
+            }
+        }
+    }
+
+    private fun setComponentFields(stmt: PreparedStatement, obj: Component): Int {
+        with(obj) {
+            stmt.setStringSafe(1, name)
+            stmt.setStringSafe(2, node)
+            stmt.setStringSafe(3, color.hex)
+            stmt.setInt(4, ordinal)
+            stmt.setStringOrNull(5, description)
+            stmt.setIntOrNull(6, obj.lead?.id)
+        }
+        return 7
+    }
+
+    //language=SQL
+    private val componentQuery =
+        """
+        select id, project, name, node, color, ordinal, description,
+            userid, username, givenname, lastname, mail
+        from lpit_component
+        left join lpit_user on lead = userid
+        """
+
+    private val stmtComponents by lazy {
+        connection.prepareStatement(
+            """${componentQuery}
+            where project = ?
+            order by ordinal, lower(name)
+            """
+        )
+    }
+    private val stmtComponentById by lazy {
+        connection.prepareStatement(
+            """${componentQuery}
+            where id = ?
+            """
+        )
+    }
+    private val stmtComponentByNode by lazy {
+        connection.prepareStatement(
+            """${componentQuery}
+            where project = ? and node = ?
+            """
+        )
+    }
+    private val stmtInsertComponent by lazy {
+        connection.prepareStatement(
+            """
+            insert into lpit_component (name, node, color, ordinal, description, lead, project)
+            values (?, ?, ?, ?, ?, ?, ?)
+            """
+        )
+    }
+    private val stmtUpdateComponent by lazy {
+        connection.prepareStatement(
+            "update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, lead = ? where id = ?"
+        )
+    }
+
+    override fun listComponents(project: Project): List<Component> {
+        stmtComponents.setInt(1, project.id)
+        return selectComponents(stmtComponents).toList()
+    }
+
+    override fun findComponent(id: Int): Component? {
+        stmtComponentById.setInt(1, id)
+        return selectComponents(stmtComponentById).firstOrNull()
+    }
+
+    override fun findComponentByNode(project: Project, node: String): Component? {
+        stmtComponentByNode.setInt(1, project.id)
+        stmtComponentByNode.setString(2, node)
+        return selectComponents(stmtComponentByNode).firstOrNull()
+    }
+
+    override fun insertComponent(component: Component) {
+        val col = setComponentFields(stmtInsertComponent, component)
+        stmtInsertComponent.setInt(col, component.projectid)
+        stmtInsertComponent.execute()
+    }
+
+    override fun updateComponent(component: Component) {
+        val col = setComponentFields(stmtUpdateComponent, component)
+        stmtUpdateComponent.setInt(col, component.id)
+        stmtUpdateComponent.execute()
+    }
+
+    //</editor-fold>
+
+    //<editor-fold desc="Project">
+
+    private fun selectProjects(stmt: PreparedStatement) = sequence {
+        stmt.executeQuery().use { rs ->
+            while (rs.next()) {
+                yield(Project(rs.getInt("projectid")).apply {
+                    name = rs.getString("name")
+                    node = rs.getString("node")
+                    description = rs.getString("description")
+                    repoUrl = rs.getString("repourl")
+                    owner = selectUserInfo(rs)
+                })
+            }
+        }
+    }
+
+    private fun setProjectFields(stmt: PreparedStatement, obj: Project): Int {
+        with(obj) {
+            stmt.setStringSafe(1, name)
+            stmt.setStringSafe(2, node)
+            stmt.setStringOrNull(3, description)
+            stmt.setStringOrNull(4, repoUrl)
+            stmt.setIntOrNull(5, owner?.id)
+        }
+        return 6
+    }
+
+    //language=SQL
+    private val projectQuery =
+        """
+        select projectid, name, node, description, repourl,
+            userid, username, lastname, givenname, mail
+        from lpit_project
+        left join lpit_user owner on lpit_project.owner = owner.userid
+        """
+
+    private val stmtProjects by lazy {
+        connection.prepareStatement(
+            """${projectQuery}
+            order by lower(name)
+            """
+        )
+    }
+    private val stmtProjectByID by lazy {
+        connection.prepareStatement(
+            """${projectQuery}
+            where projectid = ?
+            """
+        )
+    }
+    private val stmtProjectByNode by lazy {
+        connection.prepareStatement(
+            """${projectQuery}
+            where node = ?
+            """
+        )
+    }
+    private val stmtInsertProject by lazy {
+        connection.prepareStatement(
+            "insert into lpit_project (name, node, description, repourl, owner) values (?, ?, ?, ?, ?)"
+        )
+    }
+    private val stmtUpdateProject by lazy {
+        connection.prepareStatement(
+            "update lpit_project set name = ?, node = ?, description = ?, repourl = ?, owner = ? where projectid = ?"
+        )
+    }
+    private val stmtIssueSummary by lazy {
+        connection.prepareStatement(
+            """
+            select phase, count(*) as total
+            from lpit_issue
+            join lpit_issue_phases using(status)
+            where project = ?
+            group by phase  
+            """
+        )
+    }
+
+    override fun listProjects(): List<Project> {
+        return selectProjects(stmtProjects).toList()
+    }
+
+    override fun findProject(id: Int): Project? {
+        stmtProjectByID.setInt(1, id)
+        return selectProjects(stmtProjectByID).firstOrNull()
+    }
+
+    override fun findProjectByNode(node: String): Project? {
+        stmtProjectByNode.setString(1, node)
+        return selectProjects(stmtProjectByNode).firstOrNull()
+    }
+
+    override fun insertProject(project: Project) {
+        setProjectFields(stmtInsertProject, project)
+        stmtInsertProject.execute()
+    }
+
+    override fun updateProject(project: Project) {
+        val col = setProjectFields(stmtUpdateProject, project)
+        stmtUpdateProject.setInt(col, project.id)
+        stmtUpdateProject.execute()
+    }
+
+    override fun collectIssueSummary(project: Project): IssueSummary {
+        stmtIssueSummary.setInt(1, project.id)
+        return stmtIssueSummary.executeQuery().use { rs ->
+            val summary = IssueSummary()
+            while (rs.next()) {
+                val phase = rs.getInt("phase")
+                val total = rs.getInt("total")
+                when (phase) {
+                    0 -> summary.open = total
+                    1 -> summary.active = total
+                    2 -> summary.done = total
+                }
+            }
+            summary
+        }
+    }
+
+    //</editor-fold>
+
+    //<editor-fold desc="Issue">
+
+    private fun selectIssues(stmt: PreparedStatement) = sequence {
+        stmt.executeQuery().use { rs ->
+            while (rs.next()) {
+                val proj = Project(rs.getInt("project")).apply {
+                    name = rs.getString("projectname")
+                    node = rs.getString("projectnode")
+                }
+                val comp = rs.getInt("component").let {
+                    if (rs.wasNull()) null else
+                        Component(it, proj.id).apply {
+                            name = rs.getString("componentname")
+                            node = rs.getString("componentnode")
+                        }
+                }
+                val issue = Issue(rs.getInt("issueid"), proj, comp).apply {
+                    component = comp
+                    status = rs.getEnum("status")
+                    category = rs.getEnum("category")
+                    subject = rs.getString("subject")
+                    description = rs.getString("description")
+                    assignee = selectUserInfo(rs)
+                    created = rs.getTimestamp("created")
+                    updated = rs.getTimestamp("updated")
+                    eta = rs.getDate("eta")
+                }
+                queryAffectedVersions.setInt(1, issue.id)
+                issue.affectedVersions = selectVersions(queryAffectedVersions).toList()
+                queryResolvedVersions.setInt(1, issue.id)
+                issue.resolvedVersions = selectVersions(queryResolvedVersions).toList()
+                yield(issue)
+            }
+        }
+    }
+
+    private fun setIssueFields(stmt: PreparedStatement, obj: Issue): Int {
+        with(obj) {
+            stmt.setIntOrNull(1, component?.id)
+            stmt.setEnum(2, status)
+            stmt.setEnum(3, category)
+            stmt.setStringSafe(4, subject)
+            stmt.setStringOrNull(5, description)
+            stmt.setIntOrNull(6, assignee?.id)
+            stmt.setDateOrNull(7, eta)
+        }
+        return 8
+    }
+
+    //language=SQL
+    private val issueQuery =
+        """
+        select issueid,
+            i.project, p.name as projectname, p.node as projectnode,
+            component, c.name as componentname, c.node as componentnode,
+            status, category, subject, i.description,
+            userid, username, givenname, lastname, mail,
+            created, updated, eta
+        from lpit_issue i
+        join lpit_project p on i.project = projectid
+        left join lpit_component c on component = c.id
+        left join lpit_user on userid = assignee 
+        """
+
+    private val queryResolvedVersions by lazy {
+        connection.prepareStatement(
+            """
+            select versionid, project, name, status, ordinal, node
+            from lpit_version v join lpit_issue_resolved_version using (versionid)
+            where issueid = ?
+            order by ordinal, name
+            """
+        )
+    }
+
+    private val queryAffectedVersions by lazy {
+        connection.prepareStatement(
+            """
+            select versionid, project, name, status, ordinal, node
+            from lpit_version join lpit_issue_affected_version using (versionid)
+            where issueid = ?
+            order by ordinal, name
+            """
+        )
+    }
+
+    private val stmtIssues by lazy {
+        connection.prepareStatement(
+            """
+            with issue_version as (
+                select issueid, versionid from lpit_issue_affected_version
+                union select issueid, versionid from lpit_issue_resolved_version
+            ) ${issueQuery} left join issue_version using (issueid)
+            where
+            (not ? or projectid = ?) and 
+            (not ? or versionid = ?) and (not ? or versionid is null) and
+            (not ? or component = ?) and (not ? or component is null)
+            """
+        )
+    }
+
+    private val fproj = 1
+    private val projectid = 2
+    private val fversion = 3
+    private val versionid = 4
+    private val nversion = 5
+    private val fcomp = 6
+    private val component = 7
+    private val ncomp = 8
+
+    private fun <T : Entity> applyFilter(filter: Filter<T>, fflag: Int, nflag: Int, idcol: Int) {
+        when (filter) {
+            is AllFilter -> {
+                stmtIssues.setBoolean(fflag, false)
+                stmtIssues.setBoolean(nflag, false)
+                stmtIssues.setInt(idcol, 0)
+            }
+            is NoneFilter -> {
+                stmtIssues.setBoolean(fflag, false)
+                stmtIssues.setBoolean(nflag, true)
+                stmtIssues.setInt(idcol, 0)
+            }
+            is SpecificFilter -> {
+                stmtIssues.setBoolean(fflag, true)
+                stmtIssues.setBoolean(nflag, false)
+                stmtIssues.setInt(idcol, filter.obj.id)
+            }
+            else -> {
+                TODO("Implement range filter.")
+            }
+        }
+    }
+
+    override fun listIssues(filter: IssueFilter): List<Issue> {
+        when (filter.project) {
+            is AllFilter -> {
+                stmtIssues.setBoolean(fproj, false)
+                stmtIssues.setInt(projectid, 0)
+            }
+            is SpecificFilter -> {
+                stmtIssues.setBoolean(fproj, true)
+                stmtIssues.setInt(projectid, filter.project.obj.id)
+            }
+            else -> throw IllegalArgumentException()
+        }
+        applyFilter(filter.version, fversion, nversion, versionid)
+        applyFilter(filter.component, fcomp, ncomp, component)
+
+        return selectIssues(stmtIssues).toList()
+    }
+
+    private val stmtFindIssueByID by lazy {
+        connection.prepareStatement(
+            """${issueQuery}
+            where issueid = ?
+            """
+        )
+    }
+    private val stmtInsertIssue by lazy {
+        connection.prepareStatement(
+            """
+            insert into lpit_issue (component, status, category, subject, description, assignee, eta, project)
+            values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?)
+            returning issueid
+            """
+        )
+    }
+    private val stmtUpdateIssue by lazy {
+        connection.prepareStatement(
+            """
+            update lpit_issue set updated = now(),
+                component = ?, status = ?::issue_status, category = ?::issue_category, subject = ?,
+                description = ?, assignee = ?, eta = ?
+            where issueid = ?
+            """
+        )
+    }
+    private val stmtInsertAffectedVersion by lazy {
+        connection.prepareStatement(
+            "insert into lpit_issue_affected_version (issueid, versionid) values (?,?)"
+        )
+    }
+    private val stmtInsertResolvedVersion by lazy {
+        connection.prepareStatement(
+            "insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)"
+        )
+    }
+    private val stmtClearAffectedVersions by lazy {
+        connection.prepareStatement("delete from lpit_issue_affected_version where issueid = ?")
+    }
+    private val stmtClearResolvedVersions by lazy {
+        connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?")
+    }
+
+    override fun findIssue(id: Int): Issue? {
+        stmtFindIssueByID.setInt(1, id)
+        return selectIssues(stmtFindIssueByID).firstOrNull()
+    }
+
+    private fun insertVersionInfo(issue: Issue) {
+        stmtInsertAffectedVersion.setInt(1, issue.id)
+        stmtInsertResolvedVersion.setInt(1, issue.id)
+        issue.affectedVersions.forEach {
+            stmtInsertAffectedVersion.setInt(2, it.id)
+            stmtInsertAffectedVersion.execute()
+        }
+        issue.resolvedVersions.forEach {
+            stmtInsertResolvedVersion.setInt(2, it.id)
+            stmtInsertResolvedVersion.execute()
+        }
+    }
+
+    override fun insertIssue(issue: Issue) {
+        val col = setIssueFields(stmtInsertIssue, issue)
+        stmtInsertIssue.setInt(col, issue.project.id)
+        stmtInsertIssue.executeQuery().use { rs ->
+            rs.next()
+            issue.id = rs.getInt(1)
+        }
+        insertVersionInfo(issue)
+    }
+
+    override fun updateIssue(issue: Issue) {
+        val col = setIssueFields(stmtUpdateIssue, issue)
+        stmtUpdateIssue.setInt(col, issue.id)
+        // TODO: improve by only inserting / deleting changed version information
+        stmtClearAffectedVersions.setInt(1, issue.id)
+        stmtClearResolvedVersions.setInt(1, issue.id)
+        stmtClearAffectedVersions.execute()
+        stmtClearResolvedVersions.execute()
+        insertVersionInfo(issue)
+    }
+
+    //</editor-fold>
+
+    //<editor-fold desc="IssueComment">
+
+    private fun selectComments(stmt: PreparedStatement) = sequence {
+        stmt.executeQuery().use { rs ->
+            while (rs.next()) {
+                yield(IssueComment(rs.getInt("commentid"), rs.getInt("issueid")).apply {
+                    created = rs.getTimestamp("created")
+                    updated = rs.getTimestamp("updated")
+                    updateCount = rs.getInt("updatecount")
+                    comment = rs.getString("comment")
+                    author = selectUserInfo(rs)
+                })
+            }
+        }
+    }
+
+    private val stmtComments by lazy {
+        connection.prepareStatement(
+            "select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created"
+        )
+    }
+    private val stmtInsertComment by lazy {
+        connection.prepareStatement(
+            "insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)"
+        )
+    }
+    private val stmtUpdateIssueDate by lazy {
+        connection.prepareStatement(
+            "update lpit_issue set updated = now() where issueid = ?"
+        )
+    }
+
+    override fun listComments(issue: Issue): List<IssueComment> {
+        stmtComments.setInt(1, issue.id)
+        return selectComments(stmtComments).toList()
+    }
+
+    override fun insertComment(issueComment: IssueComment) {
+        with(issueComment) {
+            stmtUpdateIssueDate.setInt(1, issueid)
+            stmtInsertComment.setInt(1, issueid)
+            stmtInsertComment.setStringSafe(2, comment)
+            stmtInsertComment.setIntOrNull(3, author?.id)
+        }
+        stmtInsertComment.execute()
+        stmtUpdateIssueDate.execute()
+    }
+    //</editor-fold>
+}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGComponentDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-/*
- * Copyright 2020 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.postgres
-
-import de.uapcore.lightpit.dao.AbstractComponentDao
-import de.uapcore.lightpit.dao.Functions
-import de.uapcore.lightpit.entities.Component
-import de.uapcore.lightpit.entities.Project
-import de.uapcore.lightpit.entities.User
-import de.uapcore.lightpit.types.WebColor
-import java.sql.Connection
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-
-class PGComponentDao(connection: Connection) : AbstractComponentDao() {
-
-    private val query = "select id, name, node, color, ordinal, description, " +
-            "userid, username, givenname, lastname, mail " +
-            "from lpit_component " +
-            "left join lpit_user on lead = userid"
-
-    private val listStmt = connection.prepareStatement("$query where project = ? order by ordinal, lower(name)")
-    private val findStmt = connection.prepareStatement("$query where id = ? ")
-    private val findByNodeStmt = connection.prepareStatement("$query where project = ? and node = ?")
-    private val insertStmt = connection.prepareStatement(
-            "insert into lpit_component (name, node, color, ordinal, description, lead, project) values (?, ?, ?, ?, ?, ?, ?)"
-    )
-    private val updateStmt = connection.prepareStatement(
-            "update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, lead = ? where id = ?"
-    )
-
-    override fun mapResult(rs: ResultSet): Component {
-        val component = Component(rs.getInt("id"))
-        component.name = rs.getString("name")
-        component.node = rs.getString("node")
-        component.color = try {
-            WebColor(rs.getString("color"))
-        } catch (ex: IllegalArgumentException) {
-            WebColor("000000")
-        }
-        component.ordinal = rs.getInt("ordinal")
-        component.description = rs.getString("description")
-        component.lead = PGUserDao.mapResult(rs).takeUnless { rs.wasNull() }
-        return component
-    }
-
-    private fun setColumns(stmt: PreparedStatement, instance: Component): Int {
-        var column = 0
-        stmt.setString(++column, instance.name)
-        stmt.setString(++column, instance.node)
-        stmt.setString(++column, instance.color.hex)
-        stmt.setInt(++column, instance.ordinal)
-        Functions.setStringOrNull(stmt, ++column, instance.description)
-        setForeignKeyOrNull(stmt, ++column, instance.lead, User::id)
-        return column
-    }
-
-    override fun save(instance: Component, parent: Project) {
-        var column = setColumns(insertStmt, instance)
-        insertStmt.setInt(++column, parent.id)
-        insertStmt.executeUpdate()
-    }
-
-    override fun update(instance: Component): Boolean {
-        var column = setColumns(updateStmt, instance)
-        updateStmt.setInt(++column, instance.id)
-        return updateStmt.executeUpdate() > 0
-    }
-
-
-    override fun list(parent: Project): List<Component> {
-        listStmt.setInt(1, parent.id)
-        return super.list(listStmt)
-    }
-
-    override fun find(id: Int): Component? {
-        findStmt.setInt(1, id)
-        return super.find(findStmt)
-    }
-
-    override fun findByNode(parent: Project, node: String): Component? {
-        findByNodeStmt.setInt(1, parent.id)
-        findByNodeStmt.setString(2, node)
-        return super.find(findByNodeStmt)
-    }
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGDaoProvider.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-/*
- * Copyright 2020 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.postgres
-
-import de.uapcore.lightpit.dao.DaoProvider
-import java.sql.Connection
-
-class PGDaoProvider(connection: Connection) : DaoProvider {
-    override val userDao = PGUserDao(connection)
-    override val projectDao = PGProjectDao(connection)
-    override val componentDao = PGComponentDao(connection)
-    override val versionDao = PGVersionDao(connection)
-    override val issueDao = PGIssueDao(connection)
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGIssueDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,255 +0,0 @@
-/*
- * Copyright 2020 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.postgres
-
-import de.uapcore.lightpit.dao.AbstractIssueDao
-import de.uapcore.lightpit.dao.Functions
-import de.uapcore.lightpit.entities.*
-import java.sql.Connection
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.sql.Types
-
-class PGIssueDao(connection: Connection) : AbstractIssueDao() {
-
-    private val query = "select issueid, i.project, p.name as projectname, p.node as projectnode, " +
-            "component, c.name as componentname, c.node as componentnode, " +
-            "status, category, subject, i.description, " +
-            "userid, username, givenname, lastname, mail, " +
-            "created, updated, eta " +
-            "from lpit_issue i " +
-            "join lpit_project p on i.project = projectid " +
-            "left join lpit_component c on component = c.id " +
-            "left join lpit_user on userid = assignee "
-    private val list = connection.prepareStatement(query +
-            "where i.project = ? and coalesce(component, -1) = coalesce(?, component, -1)")
-    private val listForVersion = connection.prepareStatement(
-            "with issue_version as ( " +
-                    "select issueid, versionid from lpit_issue_affected_version union " +
-                    "select issueid, versionid from lpit_issue_resolved_version) " +
-                    query +
-                    "left join issue_version using (issueid) " +
-                    "where i.project = ? " +
-                    "and coalesce(versionid,-1) = ? and coalesce(component, -1) = coalesce(?, component, -1)"
-    )
-    private val find = connection.prepareStatement(query + "where issueid = ? ")
-    private val insert = connection.prepareStatement(
-            "insert into lpit_issue (project, component, status, category, subject, description, assignee, eta) " +
-                    "values (?, ?, ?::issue_status, ?::issue_category, ?, ?, ?, ?) returning issueid"
-    )
-    private val update = connection.prepareStatement(
-            "update lpit_issue set " +
-                    "updated = now(), component = ?, status = ?::issue_status, category = ?::issue_category, " +
-                    "subject = ?, description = ?, assignee = ?, eta = ? where issueid = ?"
-    )
-    private val affectedVersions = connection.prepareStatement(
-            "select versionid, name, status, ordinal, node " +
-                    "from lpit_version join lpit_issue_affected_version using (versionid) " +
-                    "where issueid = ? " +
-                    "order by ordinal, name"
-    )
-    private val clearAffected = connection.prepareStatement("delete from lpit_issue_affected_version where issueid = ?")
-    private val insertAffected = connection.prepareStatement("insert into lpit_issue_affected_version (issueid, versionid) values (?,?)")
-
-    private val resolvedVersions = connection.prepareStatement(
-            "select versionid, name, status, ordinal, node " +
-                    "from lpit_version v join lpit_issue_resolved_version using (versionid) " +
-                    "where issueid = ? " +
-                    "order by ordinal, name"
-    )
-    private val clearResolved = connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?")
-    private val insertResolved = connection.prepareStatement("insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)")
-    private val insertComment = connection.prepareStatement(
-            "insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)"
-    )
-    private val updateComment = connection.prepareStatement(
-            "update lpit_issue_comment set comment = ?, updated = now(), updatecount = updatecount+1 where commentid = ?"
-    )
-    private val listComments = connection.prepareStatement(
-            "select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created"
-    )
-
-    private val updateIssueLastModified = connection.prepareStatement(
-        "update lpit_issue set updated = now() where issueid = ?"
-    );
-
-    override fun mapResult(rs: ResultSet): Issue {
-        val project = Project(rs.getInt("project"))
-        project.name = rs.getString("projectname")
-        project.node = rs.getString("projectnode")
-        val issue = Issue(rs.getInt("issueid"))
-        issue.project = project
-        issue.component = rs.getInt("component").let { id ->
-            if (rs.wasNull()) {
-                null
-            } else {
-                val component = Component(id)
-                component.name = rs.getString("componentname")
-                component.node = rs.getString("componentnode")
-                component
-            }
-        }
-        issue.status = IssueStatus.valueOf(rs.getString("status"))
-        issue.category = IssueCategory.valueOf(rs.getString("category"))
-        issue.subject = rs.getString("subject")
-        issue.description = rs.getString("description")
-        issue.assignee = PGUserDao.mapResult(rs).takeUnless { rs.wasNull() }
-        issue.created = rs.getTimestamp("created")
-        issue.updated = rs.getTimestamp("updated")
-        issue.eta = rs.getDate("eta")
-        return issue
-    }
-
-    private fun updateVersionLists(instance: Issue) {
-        clearAffected.setInt(1, instance.id)
-        clearResolved.setInt(1, instance.id)
-        insertAffected.setInt(1, instance.id)
-        insertResolved.setInt(1, instance.id)
-        clearAffected.executeUpdate()
-        clearResolved.executeUpdate()
-        for (v: Version in instance.affectedVersions) {
-            insertAffected.setInt(2, v.id)
-            insertAffected.executeUpdate()
-        }
-        for (v: Version in instance.resolvedVersions) {
-            insertResolved.setInt(2, v.id)
-            insertResolved.executeUpdate()
-        }
-    }
-
-    private fun setData(stmt: PreparedStatement, column: Int, instance: Issue): Int {
-        var col = column
-        setForeignKeyOrNull(stmt, ++col, instance.component, Component::id)
-        stmt.setString(++col, instance.status.name)
-        stmt.setString(++col, instance.category.name)
-        stmt.setString(++col, instance.subject)
-        Functions.setStringOrNull(stmt, ++col, instance.description)
-        setForeignKeyOrNull(stmt, ++col, instance.assignee, User::id)
-        Functions.setDateOrNull(stmt, ++col, instance.eta)
-        return col
-    }
-
-    override fun save(instance: Issue, parent: Project) {
-        instance.project = parent
-        var column = 0
-        insert.setInt(++column, parent.id)
-        setData(insert, column, instance)
-        // insert and retrieve the ID
-        val rs = insert.executeQuery()
-        rs.next()
-        instance.id = rs.getInt(1)
-        updateVersionLists(instance)
-    }
-
-    override fun update(instance: Issue): Boolean {
-        var column = setData(update, 0, instance)
-        update.setInt(++column, instance.id)
-        return if (update.executeUpdate() > 0) {
-            updateVersionLists(instance)
-            true
-        } else {
-            false
-        }
-    }
-
-    override fun list(parent: Project): List<Issue> {
-        list.setInt(1, parent.id)
-        list.setNull(2, Types.INTEGER)
-        return super.list(list)
-    }
-
-    override fun list(project: Project, component: Component?, version: Version?): List<Issue> {
-        listForVersion.setInt(1, project.id)
-        listForVersion.setInt(2, version?.id ?: -1)
-        listForVersion.setInt(3, component?.id ?: -1)
-        return super.list(listForVersion)
-    }
-
-    override fun list(project: Project, version: Version?): List<Issue> {
-        listForVersion.setInt(1, project.id)
-        listForVersion.setInt(2, version?.id ?: -1)
-        listForVersion.setNull(3, Types.INTEGER)
-        return super.list(listForVersion)
-    }
-
-    override fun list(project: Project, component: Component?): List<Issue> {
-        list.setInt(1, project.id)
-        list.setInt(2, component?.id ?: -1)
-        return super.list(list)
-    }
-
-    override fun find(id: Int): Issue? {
-        find.setInt(1, id)
-        return super.find(find)
-    }
-
-    private fun listVersions(stmt: PreparedStatement, issue: Issue): List<Version> {
-        stmt.setInt(1, issue.id)
-        return sequence {
-            stmt.executeQuery().use { result ->
-                while (result.next()) yield(PGVersionDao.mapResult(result))
-            }
-        }.toList()
-    }
-
-    override fun joinVersionInformation(issue: Issue) {
-        issue.affectedVersions = listVersions(affectedVersions, issue)
-        issue.resolvedVersions = listVersions(resolvedVersions, issue)
-    }
-
-    override fun listComments(issue: Issue): List<IssueComment> {
-        listComments.setInt(1, issue.id)
-        return sequence {
-            listComments.executeQuery().use { rs ->
-                while (rs.next()) {
-                    val comment = IssueComment(rs.getInt("commentid"))
-                    comment.created = rs.getTimestamp("created")
-                    comment.updated = rs.getTimestamp("updated")
-                    comment.updateCount = rs.getInt("updatecount")
-                    comment.comment = rs.getString("comment")
-                    comment.author = PGUserDao.mapResult(rs).takeUnless { rs.wasNull() }
-                    yield(comment)
-                }
-            }
-        }.toList()
-    }
-
-    override fun saveComment(issue: Issue, comment: IssueComment) {
-        if (comment.id >= 0) {
-            updateComment.setString(1, comment.comment)
-            updateComment.setInt(2, comment.id)
-            updateComment.execute()
-        } else {
-            insertComment.setInt(1, issue.id)
-            insertComment.setString(2, comment.comment)
-            setForeignKeyOrNull(insertComment, 3, comment.author, User::id)
-            insertComment.execute()
-        }
-        updateIssueLastModified.setInt(1, issue.id);
-        updateIssueLastModified.execute();
-    }
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGProjectDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/*
- * Copyright 2020 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.postgres
-
-import de.uapcore.lightpit.dao.AbstractProjectDao
-import de.uapcore.lightpit.dao.Functions
-import de.uapcore.lightpit.entities.IssueSummary
-import de.uapcore.lightpit.entities.Project
-import de.uapcore.lightpit.entities.User
-import java.sql.Connection
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-
-class PGProjectDao(connection: Connection) : AbstractProjectDao() {
-
-    private val query = "select projectid, name, node, description, repourl, " +
-            "userid, username, lastname, givenname, mail " +
-            "from lpit_project " +
-            "left join lpit_user owner on lpit_project.owner = owner.userid "
-
-    private val listStmt = connection.prepareStatement("$query order by name")
-    private val findStmt = connection.prepareStatement("$query where projectid = ?")
-    private val findByNodeStmt = connection.prepareStatement("$query where node = ?")
-    private val issueSummaryStmt = connection.prepareStatement(
-            "select phase, count(*) as total " +
-                    "from lpit_issue " +
-                    "join lpit_issue_phases using(status) " +
-                    "where project = ? " +
-                    "group by phase "
-    )
-    private val insertStmt = connection.prepareStatement(
-            "insert into lpit_project (name, node, description, repourl, owner) values (?, ?, ?, ?, ?)"
-    )
-    private val updateStmt = connection.prepareStatement(
-            "update lpit_project set name = ?, node = ?, description = ?, repourl = ?, owner = ? where projectid = ?"
-    )
-
-    override fun mapResult(rs: ResultSet): Project {
-        val proj = Project(rs.getInt("projectid"))
-        proj.name = rs.getString("name")
-        proj.node = rs.getString("node")
-        proj.description = rs.getString("description")
-        proj.repoUrl = rs.getString("repourl")
-        proj.owner = PGUserDao.mapResult(rs).takeUnless { rs.wasNull() }
-        return proj
-    }
-
-    override fun getIssueSummary(project: Project): IssueSummary {
-        issueSummaryStmt.setInt(1, project.id)
-        val result = issueSummaryStmt.executeQuery()
-        val summary = IssueSummary()
-        while (result.next()) {
-            val phase = result.getInt("phase")
-            val total = result.getInt("total")
-            when (phase) {
-                0 -> summary.open = total
-                1 -> summary.active = total
-                2 -> summary.done = total
-            }
-        }
-        return summary
-    }
-
-    private fun setColumns(stmt: PreparedStatement, instance: Project): Int {
-        var column = 0
-        stmt.setString(++column, instance.name)
-        stmt.setString(++column, instance.node)
-        Functions.setStringOrNull(stmt, ++column, instance.description)
-        Functions.setStringOrNull(stmt, ++column, instance.repoUrl)
-        setForeignKeyOrNull(stmt, ++column, instance.owner, User::id)
-        return column
-    }
-
-    override fun save(instance: Project) {
-        setColumns(insertStmt, instance)
-        insertStmt.executeUpdate()
-    }
-
-    override fun update(instance: Project): Boolean {
-        var column = setColumns(updateStmt, instance)
-        updateStmt.setInt(++column, instance.id)
-        return updateStmt.executeUpdate() > 0
-    }
-
-    override fun list(): List<Project> {
-        return super.list(listStmt)
-    }
-
-    override fun find(id: Int): Project? {
-        findStmt.setInt(1, id)
-        return super.find(findStmt)
-    }
-
-    override fun findByNode(node: String): Project? {
-        findByNodeStmt.setString(1, node)
-        return super.find(findByNodeStmt)
-    }
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGUserDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-/*
- * Copyright 2020 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.postgres
-
-import de.uapcore.lightpit.dao.AbstractUserDao
-import de.uapcore.lightpit.dao.Functions
-import de.uapcore.lightpit.entities.User
-import java.sql.Connection
-import java.sql.ResultSet
-
-class PGUserDao(connection: Connection) : AbstractUserDao() {
-
-    companion object {
-        fun mapResult(rs: ResultSet): User {
-            val id = rs.getInt("userid")
-            return if (rs.wasNull()) {
-                User(-1)
-            } else {
-                val user = User(id)
-                user.username = rs.getString("username")
-                user.givenname = Functions.getSafeString(rs, "givenname")
-                user.lastname = Functions.getSafeString(rs, "lastname")
-                user.mail = Functions.getSafeString(rs, "mail")
-                user
-            }
-        }
-    }
-
-    private val listStmt = connection.prepareStatement(
-            "select userid, username, lastname, givenname, mail " +
-                    "from lpit_user where userid >= 0 " +
-                    "order by username")
-    private val findStmt = connection.prepareStatement(
-            "select userid, username, lastname, givenname, mail " +
-                    "from lpit_user where userid = ? ")
-    private val findByUsernameStmt = connection.prepareStatement(
-            "select userid, username, lastname, givenname, mail " +
-                    "from lpit_user where lower(username) = lower(?) ")
-    private val insertStmt = connection.prepareStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)")
-    private val updateStmt = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?")
-
-    override fun mapResult(rs: ResultSet): User = Companion.mapResult(rs)
-
-    override fun save(instance: User) {
-        insertStmt.setString(1, instance.username)
-        Functions.setStringOrNull(insertStmt, 2, instance.lastname)
-        Functions.setStringOrNull(insertStmt, 3, instance.givenname)
-        Functions.setStringOrNull(insertStmt, 4, instance.mail)
-        insertStmt.executeUpdate()
-    }
-
-    override fun update(instance: User): Boolean {
-        Functions.setStringOrNull(updateStmt, 1, instance.lastname)
-        Functions.setStringOrNull(updateStmt, 2, instance.givenname)
-        Functions.setStringOrNull(updateStmt, 3, instance.mail)
-        updateStmt.setInt(4, instance.id)
-        return updateStmt.executeUpdate() > 0
-    }
-
-    override fun list(): List<User> = super.list(listStmt)
-
-    override fun find(id: Int): User? {
-        findStmt.setInt(1, id)
-        return super.find(findStmt)
-    }
-
-    override fun findByUsername(username: String): User? {
-        findByUsernameStmt.setString(1, username)
-        return super.find(findByUsernameStmt)
-    }
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGVersionDao.kt	Sun Dec 20 11:06:25 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/*
- * Copyright 2020 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.postgres
-
-import de.uapcore.lightpit.dao.AbstractVersionDao
-import de.uapcore.lightpit.entities.Project
-import de.uapcore.lightpit.entities.Version
-import de.uapcore.lightpit.entities.VersionStatus
-import java.sql.Connection
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-
-class PGVersionDao(connection: Connection) : AbstractVersionDao() {
-
-    companion object {
-        fun mapResult(rs: ResultSet): Version {
-            val id = rs.getInt("versionid")
-            return if (rs.wasNull()) {
-                Version(-1)
-            } else {
-                val version = Version(id)
-                version.name = rs.getString("name")
-                version.node = rs.getString("node")
-                version.ordinal = rs.getInt("ordinal")
-                version.status = VersionStatus.valueOf(rs.getString("status"))
-                version
-            }
-        }
-    }
-
-    private val query = "select versionid, project, name, node, ordinal, status from lpit_version"
-    private val listStmt = connection.prepareStatement(query + " where project = ? " +
-            "order by ordinal desc, lower(name) desc")
-    private val findStmt = connection.prepareStatement("$query where versionid = ?")
-    private val findByNodeStmt = connection.prepareStatement("$query where project = ? and node = ?")
-    private val insertStmt = connection.prepareStatement(
-            "insert into lpit_version (name, node, ordinal, status, project) values (?, ?, ?, ?::version_status, ?)"
-    )
-    private val updateStmt = connection.prepareStatement(
-            "update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status where versionid = ?"
-    )
-
-    override fun mapResult(rs: ResultSet): Version = Companion.mapResult(rs)
-
-    private fun setFields(stmt: PreparedStatement, instance: Version): Int {
-        var column = 0
-        stmt.setString(++column, instance.name)
-        stmt.setString(++column, instance.node)
-        stmt.setInt(++column, instance.ordinal)
-        stmt.setString(++column, instance.status.name)
-        return column
-    }
-
-    override fun save(instance: Version, parent: Project) {
-        var column = setFields(insertStmt, instance)
-        insertStmt.setInt(++column, parent.id)
-        insertStmt.executeUpdate()
-    }
-
-    override fun update(instance: Version): Boolean {
-        var column = setFields(updateStmt, instance)
-        updateStmt.setInt(++column, instance.id)
-        return updateStmt.executeUpdate() > 0
-    }
-
-    override fun list(parent: Project): List<Version> {
-        listStmt.setInt(1, parent.id)
-        return super.list(listStmt)
-    }
-
-    override fun find(id: Int): Version? {
-        findStmt.setInt(1, id)
-        return super.find(findStmt)
-    }
-
-    override fun findByNode(parent: Project, node: String): Version? {
-        findByNodeStmt.setInt(1, parent.id)
-        findByNodeStmt.setString(2, node)
-        return super.find(findByNodeStmt)
-    }
-}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/entities/Component.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/Component.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -27,7 +27,7 @@
 
 import de.uapcore.lightpit.types.WebColor
 
-data class Component(val id: Int) {
+data class Component(override val id: Int, var projectid: Int) : Entity {
     var name: String = ""
     var node: String = name
     var color = WebColor("000000")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/Entity.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 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
+
+interface Entity {
+    val id: Int
+}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -25,81 +25,32 @@
 
 package de.uapcore.lightpit.entities
 
+import de.uapcore.lightpit.types.IssueCategory
+import de.uapcore.lightpit.types.IssueStatus
+import de.uapcore.lightpit.types.IssueStatusPhase
 import java.sql.Date
 import java.sql.Timestamp
 import java.time.Instant
-import kotlin.math.roundToInt
 
-data class IssueStatusPhase(val number: Int) {
-    companion object {
-        val Open = IssueStatusPhase(0)
-        val WorkInProgress = IssueStatusPhase(1)
-        val Done = IssueStatusPhase(2)
-    }
-}
-
-enum class IssueStatus(val phase: IssueStatusPhase) {
-    InSpecification(IssueStatusPhase.Open),
-    ToDo(IssueStatusPhase.Open),
-    Scheduled(IssueStatusPhase.Open),
-    InProgress(IssueStatusPhase.WorkInProgress),
-    InReview(IssueStatusPhase.WorkInProgress),
-    Done(IssueStatusPhase.Done),
-    Rejected(IssueStatusPhase.Done),
-    Withdrawn(IssueStatusPhase.Done),
-    Duplicate(IssueStatusPhase.Done);
-}
-
-enum class IssueCategory {
-    Feature, Improvement, Bug, Task, Test
-}
-
-data class Issue(var id: Int) {
-
-    var project: Project? = null
-    var component: Component? = null
+data class Issue(override var id: Int, var project: Project, var component: Component? = null) : Entity {
 
     var status = IssueStatus.InSpecification
     var category = IssueCategory.Feature
 
-    var subject: String? = null
+    var subject: String = ""
     var description: String? = null
     var assignee: User? = null
 
-    var affectedVersions = emptyList<Version>()
-    var resolvedVersions = emptyList<Version>()
-
     var created: Timestamp = Timestamp.from(Instant.now())
     var updated: Timestamp = Timestamp.from(Instant.now())
     var eta: Date? = null
 
+    var affectedVersions = emptyList<Version>()
+    var resolvedVersions = emptyList<Version>()
+
     /**
      * An issue is overdue, if it is not done and the ETA is before the current time.
      */
     val overdue get() = status.phase != IssueStatusPhase.Done && eta?.before(Date(System.currentTimeMillis())) ?: false
 }
 
-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++
-        }
-    }
-}
-
--- a/src/main/kotlin/de/uapcore/lightpit/entities/IssueComment.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueComment.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -28,9 +28,9 @@
 import java.sql.Timestamp
 import java.time.Instant
 
-data class IssueComment(val id: Int) {
+data class IssueComment(override val id: Int, val issueid: Int) : Entity {
     var author: User? = null
-    var comment: String? = null
+    var comment: String = ""
     var created: Timestamp = Timestamp.from(Instant.now())
     var updated: Timestamp = Timestamp.from(Instant.now())
     var updateCount = 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueSummary.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 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
--- a/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/Project.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -25,9 +25,9 @@
 
 package de.uapcore.lightpit.entities
 
-data class Project(val id: Int) {
-    var name: String? = null
-    var node: String? = null
+data class Project(override val id: Int) : Entity {
+    var name: String = ""
+    var node: String = name
     var description: String? = null
     var repoUrl: String? = null
     var owner: User? = null
--- a/src/main/kotlin/de/uapcore/lightpit/entities/User.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/User.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -25,16 +25,17 @@
 
 package de.uapcore.lightpit.entities
 
-data class User(val id: Int) {
-    var username = ""
-    var mail = ""
-    var givenname = ""
-    var lastname = ""
+data class User(override val id: Int) : Entity {
+    var username: String = ""
+    var mail: String? = null
+    var givenname: String? = null
+    var lastname: String? = null
 
-    val shortDisplayname: String get() {
-        val str = "$givenname $lastname"
-        return if (str.isBlank()) username else str.trim()
-    }
+    val shortDisplayname: String
+        get() {
+            val str = "${givenname ?: ""} ${lastname ?: ""}"
+            return if (str.isBlank()) username else str.trim()
+        }
 
-    val displayname: String get() = if (mail.isBlank()) shortDisplayname else "$shortDisplayname <$mail>"
+    val displayname: String get() = if (mail.isNullOrBlank()) shortDisplayname else "$shortDisplayname <$mail>"
 }
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/entities/Version.kt	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/Version.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -25,12 +25,9 @@
 
 package de.uapcore.lightpit.entities
 
-enum class VersionStatus {
-    Future, Unreleased, Released, LTS, Deprecated;
-    val isReleased get() = this.ordinal >= Released.ordinal
-}
+import de.uapcore.lightpit.types.VersionStatus
 
-data class Version(val id: Int) : Comparable<Version> {
+data class Version(override val id: Int, var projectid: Int) : Entity, Comparable<Version> {
     var name: String = ""
     var node = name
     var ordinal = 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/filter/Filter.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 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<T>
+class AllFilter<T> : Filter<T>()
+class NoneFilter<T> : Filter<T>()
+data class SpecificFilter<T>(val obj: T) : Filter<T>()
+data class RangeFilter<T>(val lower: T, val upper: T) : Filter<T>() where T : Comparable<T>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/filter/IssueFilter.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 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<Project> = AllFilter(),
+    val version: Filter<Version> = AllFilter(),
+    val component: Filter<Component> = AllFilter()
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/types/IssueCategory.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 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.types
+
+enum class IssueCategory {
+    Feature, Improvement, Bug, Task, Test
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/types/IssueStatus.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 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.types
+
+enum class IssueStatus(val phase: IssueStatusPhase) {
+    InSpecification(IssueStatusPhase.Open),
+    ToDo(IssueStatusPhase.Open),
+    Scheduled(IssueStatusPhase.Open),
+    InProgress(IssueStatusPhase.WorkInProgress),
+    InReview(IssueStatusPhase.WorkInProgress),
+    Done(IssueStatusPhase.Done),
+    Rejected(IssueStatusPhase.Done),
+    Withdrawn(IssueStatusPhase.Done),
+    Duplicate(IssueStatusPhase.Done);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/types/IssueStatusPhase.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 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.types
+
+data class IssueStatusPhase(val number: Int) {
+    companion object {
+        val Open = IssueStatusPhase(0)
+        val WorkInProgress = IssueStatusPhase(1)
+        val Done = IssueStatusPhase(2)
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/types/VersionStatus.kt	Mon Dec 21 18:29:34 2020 +0100
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 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.types
+
+enum class VersionStatus {
+    Future, Unreleased, Released, LTS, Deprecated;
+
+    val isReleased get() = this.ordinal >= Released.ordinal
+}
\ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Sun Dec 20 11:06:25 2020 +0100
+++ b/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Mon Dec 21 18:29:34 2020 +0100
@@ -26,7 +26,7 @@
 --%>
 <%@page pageEncoding="UTF-8"
         import="de.uapcore.lightpit.viewmodel.ProjectView"
-        import="de.uapcore.lightpit.entities.VersionStatus"
+        import="de.uapcore.lightpit.types.VersionStatus"
 %>
 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

mercurial