migrates DAO classes

Thu, 19 Nov 2020 13:58:54 +0100

author
mike@uapl01.localdomain
date
Thu, 19 Nov 2020 13:58:54 +0100
changeset 159
86b5d8a1662f
parent 158
4f912cd42876
child 160
e2d09cf3fb96

migrates DAO classes

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/ChildEntityDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/ComponentDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/DataAccessObjects.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/IssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/ProjectDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/RootEntityDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/UserDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/VersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGComponentDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/UsersModule.java file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/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/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/User.kt file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Fri Nov 06 10:50:32 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Thu Nov 19 13:58:54 2020 +0100
@@ -28,8 +28,8 @@
  */
 package de.uapcore.lightpit;
 
-import de.uapcore.lightpit.dao.DataAccessObjects;
-import de.uapcore.lightpit.dao.postgres.PGDataAccessObjects;
+import de.uapcore.lightpit.dao.DaoProvider;
+import de.uapcore.lightpit.dao.postgres.PGDaoProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -100,15 +100,15 @@
      * @param connection the SQL connection
      * @return a set of data access objects
      */
-    private DataAccessObjects createDataAccessObjects(Connection connection) throws SQLException {
+    private DaoProvider createDataAccessObjects(Connection connection) throws SQLException {
         final var df = (DataSourceProvider) getServletContext().getAttribute(DataSourceProvider.Companion.getSC_ATTR_NAME());
         if (df.getDialect() == DataSourceProvider.Dialect.Postgres) {
-            return new PGDataAccessObjects(connection);
+            return new PGDaoProvider(connection);
         }
         throw new UnsupportedOperationException("Non-exhaustive if-else - this is a bug.");
     }
 
-    private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
+    private void invokeMapping(Map.Entry<PathPattern, Method> mapping, HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException {
         final var pathPattern = mapping.getKey();
         final var method = mapping.getValue();
         try {
@@ -121,7 +121,7 @@
                 } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
                     paramValues[i] = resp;
                 }
-                if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) {
+                if (paramTypes[i].isAssignableFrom(DaoProvider.class)) {
                     paramValues[i] = dao;
                 }
                 if (paramTypes[i].isAssignableFrom(PathParameters.class)) {
@@ -180,7 +180,7 @@
                         paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
                                 || HttpServletResponse.class.isAssignableFrom(param)
                                 || PathParameters.class.isAssignableFrom(param)
-                                || DataAccessObjects.class.isAssignableFrom(param);
+                                || DaoProvider.class.isAssignableFrom(param);
                     }
                     if (paramsInjectible) {
                         try {
--- a/src/main/java/de/uapcore/lightpit/dao/ChildEntityDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +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.SQLException;
-import java.util.List;
-
-public interface ChildEntityDao<T, P> {
-
-    /**
-     * Lists all entities being a child of the specified parent.
-     * @param parent the parent
-     * @return the list of child instances
-     * @throws SQLException on any kind of SQL errors
-     */
-    List<T> list(P parent) throws SQLException;
-
-    /**
-     * Finds an entity by its integer ID.
-     * It is not guaranteed that referenced entities are automatically joined.
-     *
-     * @param id the id
-     * @return the enity or null if there is no such entity
-     * @throws SQLException on any kind of SQL errors
-     */
-    T find(int id) throws SQLException;
-
-    /**
-     * 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
-     * @throws SQLException on any kind of SQL errors
-     */
-    void save(T instance, P parent) throws SQLException;
-
-    /**
-     * Updates an instance in the database.
-     *
-     * @param instance the instance to insert
-     * @return true if an instance has been updated, false if no instance with the specified ID was found
-     * @throws SQLException on any kind of SQL errors
-     */
-    boolean update(T instance) throws SQLException;
-
-    /**
-     * Updates an instance in the database changing the parent.
-     * This operation is not supported by default.
-     *
-     * @param instance the instance to insert
-     * @param newParent a reference to the new parent
-     * @return true if an instance has been updated, false if no instance with the specified ID was found
-     * @throws SQLException on any kind of SQL errors
-     * @see #isChangingParentSupported()
-     */
-    default boolean update(T instance, P newParent) throws SQLException {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Returns true if changing the parent is supported by this DAO.
-     * This method must return true, if {@link #update(Object, Object)} is implemented.
-     * @return true, if changing the parent is supported
-     */
-    default boolean isChangingParentSupported() {
-        return false;
-    }
-
-    /**
-     * Inserts or updates an instance in the database.
-     * Tries an update first and if that fails, performs a save.
-     * If changing a parent is not supported by this DAO,
-     * specifying an alternate parent for an existing instance has no effect.
-     *
-     * @param instance the instance to insert or update
-     * @param parent a reference to the parent
-     * @throws SQLException on any kind of SQL errors
-     * @see #update(Object)
-     * @see #update(Object, Object)
-     * @see #save(Object, Object)
-     */
-    default void saveOrUpdate(T instance, P parent) throws SQLException {
-        if (isChangingParentSupported()) {
-            if (!update(instance, parent)) save(instance, parent);
-        } else {
-            if (!update(instance)) save(instance, parent);
-        }
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/ComponentDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +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 de.uapcore.lightpit.entities.Component;
-import de.uapcore.lightpit.entities.Project;
-
-import java.sql.SQLException;
-
-public interface ComponentDao extends ChildEntityDao<Component, Project> {
-    Component findByNode(Project parent, String node) throws SQLException;
-}
--- a/src/main/java/de/uapcore/lightpit/dao/DataAccessObjects.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +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;
-
-public interface DataAccessObjects {
-    UserDao getUserDao();
-
-    ProjectDao getProjectDao();
-
-    VersionDao getVersionDao();
-
-    ComponentDao getComponentDao();
-
-    IssueDao getIssueDao();
-}
--- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +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 de.uapcore.lightpit.entities.*;
-
-import java.sql.SQLException;
-import java.util.List;
-
-public interface IssueDao extends ChildEntityDao<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 or null
-     * @param version the version or null
-     * @return a list of issues
-     * @throws SQLException on any kind of SQL error
-     */
-    List<Issue> list(Project project, Component component, Version version) throws SQLException;
-
-    /**
-     * 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 (mandatory)
-     * @param version the version or null
-     * @return a list of issues
-     * @throws SQLException on any kind of SQL error
-     */
-    List<Issue> list(Project project, Version version) throws SQLException;
-
-    /**
-     * 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 (mandatory)
-     * @param component the component or null
-     * @return a list of issues
-     * @throws SQLException on any kind of SQL error
-     */
-    List<Issue> list(Project project, Component component) throws SQLException;
-
-    /**
-     * Lists all comments for a specific issue in chronological order.
-     *
-     * @param issue the issue
-     * @return the list of comments
-     * @throws SQLException on any kind of SQL error
-     */
-    List<IssueComment> listComments(Issue issue) throws SQLException;
-
-    /**
-     * Stores the specified comment in database.
-     * This is an update-or-insert operation.
-     *
-     * @param issue the issue to save the comment for
-     * @param comment the comment to save
-     * @throws SQLException on any kind of SQL error
-     */
-    void saveComment(Issue issue, IssueComment comment) throws SQLException;
-
-    /**
-     * 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 project the parent project
-     * @throws SQLException on any kind of SQL error
-     * @see Issue#setId(int)
-     */
-    @Override
-    void save(Issue instance, Project project) throws SQLException;
-
-    /**
-     * Retrieves the affected, scheduled and resolved versions for the specified issue.
-     *
-     * @param issue the issue to join the information for
-     * @throws SQLException on any kind of SQL error
-     */
-    void joinVersionInformation(Issue issue) throws SQLException;
-}
--- a/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +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 de.uapcore.lightpit.entities.IssueSummary;
-import de.uapcore.lightpit.entities.Project;
-
-import java.sql.SQLException;
-
-public interface ProjectDao extends RootEntityDao<Project> {
-    IssueSummary getIssueSummary(Project project) throws SQLException;
-
-    Project findByNode(String node) throws SQLException;
-}
--- a/src/main/java/de/uapcore/lightpit/dao/RootEntityDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +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.SQLException;
-import java.util.List;
-
-public interface RootEntityDao<T> {
-
-    /**
-     * Lists all entities.
-     * @return a list of all entities
-     * @throws SQLException on any kind of SQL errors
-     */
-    List<T> list() throws SQLException;
-
-    /**
-     * Finds an entity by its integer ID.
-     * It is not guaranteed that referenced entities are automatically joined.
-     *
-     * @param id the id
-     * @return the enity or null if there is no such entity
-     * @throws SQLException on any kind of SQL errors
-     */
-    T find(int id) throws SQLException;
-
-    /**
-     * Inserts an instance into database.
-     * It is not guaranteed that generated fields will be updated in the instance.
-     *
-     * @param instance the instance to insert
-     * @throws SQLException on any kind of SQL errors
-     */
-    void save(T instance) throws SQLException;
-
-    /**
-     * Updates an instance in the database.
-     *
-     * @param instance the instance to insert
-     * @return true if an instance has been updated, false if no instance with the specified ID was found
-     * @throws SQLException on any kind of SQL errors
-     */
-    boolean update(T instance) throws SQLException;
-
-    /**
-     * Inserts or updates an instance in the database.
-     * Tries an update first and if that fails, performs a save.
-     *
-     * @param instance the instance to insert or update
-     * @throws SQLException on any kind of SQL errors
-     * @see #update(Object)
-     * @see #save(Object)
-     */
-    default void saveOrUpdate(T instance) throws SQLException {
-        if (!update(instance)) save(instance);
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/UserDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +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 de.uapcore.lightpit.entities.User;
-
-import java.sql.SQLException;
-import java.util.Optional;
-
-public interface UserDao extends RootEntityDao<User> {
-
-    /**
-     * Tries to find a user by their username.
-     * The search is case-insensitive.
-     *
-     * @param username the username
-     * @return the user object or an empty optional if no such user exists
-     * @throws SQLException
-     */
-    Optional<User> findByUsername(String username) throws SQLException;
-}
--- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +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 de.uapcore.lightpit.entities.Project;
-import de.uapcore.lightpit.entities.Version;
-
-import java.sql.SQLException;
-
-public interface VersionDao extends ChildEntityDao<Version, Project> {
-    Version findByNode(Project parent, String node) throws SQLException;
-}
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGComponentDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +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.postgres;
-
-import de.uapcore.lightpit.dao.ComponentDao;
-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;
-import java.sql.SQLException;
-import java.util.List;
-
-public final class PGComponentDao implements ComponentDao {
-
-    private final PreparedStatement insert, update, list, find, findByNode;
-
-    public PGComponentDao(Connection connection) throws SQLException {
-        final var query = "select id, name, node, color, ordinal, description, " +
-                "userid, username, givenname, lastname, mail " +
-                "from lpit_component " +
-                "left join lpit_user on lead = userid";
-
-        list = connection.prepareStatement(query + " where project = ? order by ordinal, lower(name)");
-
-        find = connection.prepareStatement(query + " where id = ? ");
-
-        findByNode = connection.prepareStatement(query + " where project = ? and node = ?");
-
-        insert = connection.prepareStatement(
-                "insert into lpit_component (name, node, color, ordinal, description, lead, project) values (?, ?, ?, ?, ?, ?, ?)"
-        );
-
-        update = connection.prepareStatement(
-                "update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, lead = ? where id = ?"
-        );
-    }
-
-    private static Component mapColumns(ResultSet result) throws SQLException {
-        final var component = new Component(result.getInt("id"));
-        component.setName(result.getString("name"));
-        component.setNode(result.getString("node"));
-        try {
-            component.setColor(new WebColor(result.getString("color")));
-        } catch (IllegalArgumentException ex) {
-            // if someone tempered with the database we default the color to black
-            component.setColor(new WebColor("000000"));
-        }
-        component.setOrdinal(result.getInt("ordinal"));
-        component.setDescription(result.getString("description"));
-        component.setLead(PGUserDao.mapColumns(result));
-        return component;
-    }
-
-    private static int setColumns(PreparedStatement stmt, Component instance) throws SQLException {
-        int column = 0;
-        stmt.setString(++column, instance.getName());
-        stmt.setString(++column, instance.getNode());
-        stmt.setString(++column, instance.getColor().getHex());
-        stmt.setInt(++column, instance.getOrdinal());
-        Functions.setStringOrNull(stmt, ++column, instance.getDescription());
-        Functions.setForeignKeyOrNull(stmt, ++column, instance.getLead(), User::getId);
-        return column;
-    }
-
-    @Override
-    public void save(Component instance, Project project) throws SQLException {
-        int column = setColumns(insert, instance);
-        insert.setInt(++column, project.getId());
-        insert.executeUpdate();
-    }
-
-    @Override
-    public boolean update(Component instance) throws SQLException {
-        if (instance.getId() < 0) return false;
-        int column = setColumns(update, instance);
-        update.setInt(++column, instance.getId());
-        return update.executeUpdate() > 0;
-    }
-
-
-    @Override
-    public List<Component> list(Project project) throws SQLException {
-        list.setInt(1, project.getId());
-        return Functions.list(list, PGComponentDao::mapColumns);
-    }
-
-    @Override
-    public Component find(int id) throws SQLException {
-        find.setInt(1, id);
-        return Functions.find(find, PGComponentDao::mapColumns);
-    }
-
-    @Override
-    public Component findByNode(Project project, String node) throws SQLException {
-        findByNode.setInt(1, project.getId());
-        findByNode.setString(2, node);;
-        return Functions.find(findByNode, PGComponentDao::mapColumns);
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGDataAccessObjects.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +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.postgres;
-
-import de.uapcore.lightpit.dao.*;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-public class PGDataAccessObjects implements DataAccessObjects {
-
-    private final UserDao userDao;
-    private final ProjectDao projectDao;
-    private final VersionDao versionDao;
-    private final ComponentDao componentDao;
-    private final IssueDao issueDao;
-
-    public PGDataAccessObjects(Connection connection) throws SQLException {
-        userDao = new PGUserDao(connection);
-        projectDao = new PGProjectDao(connection);
-        versionDao = new PGVersionDao(connection);
-        componentDao = new PGComponentDao(connection);
-        issueDao = new PGIssueDao(connection);
-    }
-
-    @Override
-    public UserDao getUserDao() {
-        return userDao;
-    }
-
-    @Override
-    public ProjectDao getProjectDao() {
-        return projectDao;
-    }
-
-    @Override
-    public ComponentDao getComponentDao() {
-        return componentDao;
-    }
-
-    @Override
-    public VersionDao getVersionDao() {
-        return versionDao;
-    }
-
-    @Override
-    public IssueDao getIssueDao() {
-        return issueDao;
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,306 +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.postgres;
-
-import de.uapcore.lightpit.dao.IssueDao;
-import de.uapcore.lightpit.entities.*;
-
-import java.sql.*;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-import static de.uapcore.lightpit.dao.Functions.*;
-
-public final class PGIssueDao implements IssueDao {
-
-    private final PreparedStatement insert, update, list, listForVersion, find;
-    private final PreparedStatement affectedVersions, resolvedVersions;
-    private final PreparedStatement clearAffected, clearResolved;
-    private final PreparedStatement insertAffected, insertResolved;
-    private final PreparedStatement insertComment, updateComment, listComments;
-
-    public PGIssueDao(Connection connection) throws SQLException {
-        final var 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 ";
-
-        list = connection.prepareStatement(query +
-                        "where i.project = ? and coalesce(component, -1) = coalesce(?, component, -1)");
-
-        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)"
-        );
-
-        find = connection.prepareStatement(query + "where issueid = ? ");
-
-        insert = connection.prepareStatement(
-                "insert into lpit_issue (project, component, status, category, subject, description, assignee, eta) " +
-                        "values (?, ?, ?::issue_status, ?::issue_category, ?, ?, ?, ?) returning issueid"
-        );
-        update = connection.prepareStatement(
-                "update lpit_issue set " +
-                        "updated = now(), component = ?, status = ?::issue_status, category = ?::issue_category, " +
-                        "subject = ?, description = ?, assignee = ?, eta = ? where issueid = ?"
-        );
-
-        affectedVersions = connection.prepareStatement(
-                "select versionid, name, status, ordinal " +
-                        "from lpit_version join lpit_issue_affected_version using (versionid) " +
-                        "where issueid = ? " +
-                        "order by ordinal, name"
-        );
-        clearAffected = connection.prepareStatement("delete from lpit_issue_affected_version where issueid = ?");
-        insertAffected = connection.prepareStatement("insert into lpit_issue_affected_version (issueid, versionid) values (?,?)");
-
-        resolvedVersions = connection.prepareStatement(
-                "select versionid, name, status, ordinal " +
-                        "from lpit_version v join lpit_issue_resolved_version using (versionid) " +
-                        "where issueid = ? " +
-                        "order by ordinal, name"
-        );
-        clearResolved = connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?");
-        insertResolved = connection.prepareStatement("insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)");
-
-        insertComment = connection.prepareStatement(
-                "insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)"
-        );
-        updateComment = connection.prepareStatement(
-                "update lpit_issue_comment set comment = ?, updated = now(), updatecount = updatecount+1 where commentid = ?"
-        );
-        listComments = connection.prepareStatement(
-                "select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created"
-        );
-    }
-
-    private Issue mapColumns(ResultSet result) throws SQLException {
-        final var project = new Project(result.getInt("project"));
-        project.setName(result.getString("projectname"));
-        project.setNode(result.getString("projectnode"));
-        var component = new Component(result.getInt("component"));
-        if (result.wasNull()) {
-            component = null;
-        } else {
-            component.setName(result.getString("componentname"));
-            component.setNode(result.getString("componentnode"));
-        }
-        final var issue = new Issue(result.getInt("issueid"));
-        issue.setProject(project);
-        issue.setComponent(component);
-        issue.setStatus(IssueStatus.valueOf(result.getString("status")));
-        issue.setCategory(IssueCategory.valueOf(result.getString("category")));
-        issue.setSubject(result.getString("subject"));
-        issue.setDescription(result.getString("description"));
-        issue.setAssignee(PGUserDao.mapColumns(result));
-        issue.setCreated(result.getTimestamp("created"));
-        issue.setUpdated(result.getTimestamp("updated"));
-        issue.setEta(result.getDate("eta"));
-        return issue;
-    }
-
-    private Version mapVersion(ResultSet result) throws SQLException {
-        final var version = new Version(result.getInt("versionid"));
-        version.setName(result.getString("name"));
-        version.setOrdinal(result.getInt("ordinal"));
-        version.setStatus(VersionStatus.valueOf(result.getString("status")));
-        return version;
-    }
-
-    private void updateVersionLists(Issue instance) throws SQLException {
-        clearAffected.setInt(1, instance.getId());
-        clearResolved.setInt(1, instance.getId());
-        insertAffected.setInt(1, instance.getId());
-        insertResolved.setInt(1, instance.getId());
-        clearAffected.executeUpdate();
-        clearResolved.executeUpdate();
-        for (Version v : instance.getAffectedVersions()) {
-            insertAffected.setInt(2, v.getId());
-            insertAffected.executeUpdate();
-        }
-        for (Version v : instance.getResolvedVersions()) {
-            insertResolved.setInt(2, v.getId());
-            insertResolved.executeUpdate();
-        }
-    }
-
-    private int setData(PreparedStatement stmt, int column, Issue instance) throws SQLException {
-        setForeignKeyOrNull(stmt, ++column, instance.getComponent(), Component::getId);
-        stmt.setString(++column, instance.getStatus().name());
-        stmt.setString(++column, instance.getCategory().name());
-        stmt.setString(++column, instance.getSubject());
-        setStringOrNull(stmt, ++column, instance.getDescription());
-        setForeignKeyOrNull(stmt, ++column, instance.getAssignee(), User::getId);
-        setDateOrNull(stmt, ++column, instance.getEta());
-        return column;
-    }
-
-    @Override
-    public void save(Issue instance, Project project) throws SQLException {
-        Objects.requireNonNull(instance.getSubject());
-        instance.setProject(project);
-        int column = 0;
-        insert.setInt(++column, instance.getProject().getId());
-        setData(insert, column, instance);
-        // insert and retrieve the ID
-        final var rs = insert.executeQuery();
-        rs.next();
-        instance.setId(rs.getInt(1));
-        updateVersionLists(instance);
-    }
-
-    @Override
-    public boolean update(Issue instance) throws SQLException {
-        if (instance.getId() < 0) return false;
-        Objects.requireNonNull(instance.getSubject());
-        int column = setData(update, 0, instance);
-        update.setInt(++column, instance.getId());
-        boolean success = update.executeUpdate() > 0;
-        if (success) {
-            updateVersionLists(instance);
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    private List<Issue> executeQuery(PreparedStatement query) throws SQLException {
-        List<Issue> issues = new ArrayList<>();
-        try (var result = query.executeQuery()) {
-            while (result.next()) {
-                issues.add(mapColumns(result));
-            }
-        }
-        return issues;
-    }
-
-    @Override
-    public List<Issue> list(Project project) throws SQLException {
-        list.setInt(1, project.getId());
-        list.setNull(2, Types.INTEGER);
-        return executeQuery(list);
-    }
-
-    @Override
-    public List<Issue> list(Project project, Component component, Version version) throws SQLException {
-        listForVersion.setInt(1, project.getId());
-        listForVersion.setInt(2, Optional.ofNullable(version).map(Version::getId).orElse(-1));
-        listForVersion.setInt(3, Optional.ofNullable(component).map(Component::getId).orElse(-1));
-        return executeQuery(listForVersion);
-    }
-
-    @Override
-    public List<Issue> list(Project project, Version version) throws SQLException {
-        listForVersion.setInt(1, project.getId());
-        listForVersion.setInt(2, Optional.ofNullable(version).map(Version::getId).orElse(-1));
-        listForVersion.setNull(3, Types.INTEGER);
-        return executeQuery(listForVersion);
-    }
-
-    @Override
-    public List<Issue> list(Project project, Component component) throws SQLException {
-        list.setInt(1, project.getId());
-        list.setInt(2, Optional.ofNullable(component).map(Component::getId).orElse(-1));
-        return executeQuery(list);
-    }
-
-    @Override
-    public Issue find(int id) throws SQLException {
-        find.setInt(1, id);
-        try (var result = find.executeQuery()) {
-            if (result.next()) {
-                return mapColumns(result);
-            } else {
-                return null;
-            }
-        }
-    }
-
-    private List<Version> listVersions(PreparedStatement stmt, Issue issue) throws SQLException {
-        stmt.setInt(1, issue.getId());
-        List<Version> versions = new ArrayList<>();
-        try (var result = stmt.executeQuery()) {
-            while (result.next()) {
-                versions.add(mapVersion(result));
-            }
-        }
-        return versions;
-    }
-
-    @Override
-    public void joinVersionInformation(Issue issue) throws SQLException {
-        Objects.requireNonNull(issue.getProject());
-        issue.setAffectedVersions(listVersions(affectedVersions, issue));
-        issue.setResolvedVersions(listVersions(resolvedVersions, issue));
-    }
-
-    @Override
-    public List<IssueComment> listComments(Issue issue) throws SQLException {
-        listComments.setInt(1, issue.getId());
-        List<IssueComment> comments = new ArrayList<>();
-        try (var result = listComments.executeQuery()) {
-            while (result.next()) {
-                final var comment = new IssueComment(result.getInt("commentid"));
-                comment.setCreated(result.getTimestamp("created"));
-                comment.setUpdated(result.getTimestamp("updated"));
-                comment.setUpdateCount(result.getInt("updatecount"));
-                comment.setComment(result.getString("comment"));
-                comment.setAuthor(PGUserDao.mapColumns(result));
-                comments.add(comment);
-            }
-        }
-        return comments;
-    }
-
-    @Override
-    public void saveComment(Issue issue, IssueComment comment) throws SQLException {
-        if (comment.getId() >= 0) {
-            updateComment.setString(1, comment.getComment());
-            updateComment.setInt(2, comment.getId());
-            updateComment.execute();
-        } else {
-            insertComment.setInt(1, issue.getId());
-            insertComment.setString(2, comment.getComment());
-            setForeignKeyOrNull(insertComment, 3, comment.getAuthor(), User::getId);
-            insertComment.execute();
-        }
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +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.postgres;
-
-import de.uapcore.lightpit.dao.Functions;
-import de.uapcore.lightpit.dao.ProjectDao;
-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;
-import java.sql.SQLException;
-import java.util.List;
-
-import static de.uapcore.lightpit.dao.Functions.setForeignKeyOrNull;
-import static de.uapcore.lightpit.dao.Functions.setStringOrNull;
-
-public final class PGProjectDao implements ProjectDao {
-
-    private final PreparedStatement insert, update, list, find, findByNode;
-    private final PreparedStatement issue_summary;
-
-    public PGProjectDao(Connection connection) throws SQLException {
-        final var 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 ";
-
-        list = connection.prepareStatement(query + " order by name");
-
-        find = connection.prepareStatement(query + " where projectid = ?");
-        findByNode = connection.prepareStatement(query + " where node = ?");
-
-        issue_summary = connection.prepareStatement(
-                "select phase, count(*) as total "+
-                        "from lpit_issue " +
-                        "join lpit_issue_phases using(status) " +
-                        "where project = ? "+
-                        "group by phase "
-        );
-
-        insert = connection.prepareStatement(
-                "insert into lpit_project (name, node, description, repourl, owner) values (?, ?, ?, ?, ?)"
-        );
-        update = connection.prepareStatement(
-                "update lpit_project set name = ?, node = ?, description = ?, repourl = ?, owner = ? where projectid = ?"
-        );
-    }
-
-    private static Project mapColumns(ResultSet result) throws SQLException {
-        final var proj = new Project(result.getInt("projectid"));
-        proj.setName(result.getString("name"));
-        proj.setNode(result.getString("node"));
-        proj.setDescription(result.getString("description"));
-        proj.setRepoUrl(result.getString("repourl"));
-        proj.setOwner(PGUserDao.mapColumns(result));
-
-        return proj;
-    }
-
-    public IssueSummary getIssueSummary(Project project) throws SQLException {
-        issue_summary.setInt(1, project.getId());
-        final var result = issue_summary.executeQuery();
-        final var summary = new IssueSummary();
-        while (result.next()) {
-            final var phase = result.getInt("phase");
-            final var total = result.getInt("total");
-            switch(phase) {
-                case 0:
-                    summary.setOpen(total);
-                    break;
-                case 1:
-                    summary.setActive(total);
-                    break;
-                case 2:
-                    summary.setDone(total);
-                    break;
-            }
-        }
-        return summary;
-    }
-
-    private static int setColumns(PreparedStatement stmt, Project instance) throws SQLException {
-        int column = 0;
-        stmt.setString(++column, instance.getName());
-        stmt.setString(++column, instance.getNode());
-        setStringOrNull(stmt, ++column, instance.getDescription());
-        setStringOrNull(stmt, ++column, instance.getRepoUrl());
-        setForeignKeyOrNull(stmt, ++column, instance.getOwner(), User::getId);
-        return column;
-    }
-
-    @Override
-    public void save(Project instance) throws SQLException {
-        setColumns(insert, instance);
-        insert.executeUpdate();
-    }
-
-    @Override
-    public boolean update(Project instance) throws SQLException {
-        if (instance.getId() < 0) return false;
-        int column = setColumns(update, instance);
-        update.setInt(++column, instance.getId());
-        return update.executeUpdate() > 0;
-    }
-
-    @Override
-    public List<Project> list() throws SQLException {
-        return Functions.list(list, PGProjectDao::mapColumns);
-    }
-
-    @Override
-    public Project find(int id) throws SQLException {
-        find.setInt(1, id);
-        return Functions.find(find, PGProjectDao::mapColumns);
-    }
-
-    @Override
-    public Project findByNode(String node) throws SQLException {
-        findByNode.setString(1, node);
-        return Functions.find(findByNode, PGProjectDao::mapColumns);
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,132 +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.postgres;
-
-import de.uapcore.lightpit.dao.UserDao;
-import de.uapcore.lightpit.entities.User;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-import static de.uapcore.lightpit.dao.Functions.getSafeString;
-import static de.uapcore.lightpit.dao.Functions.setStringOrNull;
-
-public final class PGUserDao implements UserDao {
-
-    private final PreparedStatement insert, update, list, find, findByUsername;
-
-    public PGUserDao(Connection connection) throws SQLException {
-        list = connection.prepareStatement(
-                "select userid, username, lastname, givenname, mail " +
-                        "from lpit_user where userid >= 0 " +
-                        "order by username");
-        find = connection.prepareStatement(
-                "select userid, username, lastname, givenname, mail " +
-                        "from lpit_user where userid = ? ");
-
-        findByUsername = connection.prepareStatement(
-                "select userid, username, lastname, givenname, mail " +
-                        "from lpit_user where lower(username) = lower(?) ");
-
-        insert = connection.prepareStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)");
-        update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?");
-    }
-
-    static User mapColumns(ResultSet result) throws SQLException {
-        final int id = result.getInt("userid");
-        if (id == 0) return null;
-        final var user = new User(id);
-        user.setUsername(result.getString("username"));
-        user.setGivenname(getSafeString(result, "givenname"));
-        user.setLastname(getSafeString(result, "lastname"));
-        user.setMail(getSafeString(result, "mail"));
-        return user;
-    }
-
-    @Override
-    public void save(User instance) throws SQLException {
-        Objects.requireNonNull(instance.getUsername());
-        insert.setString(1, instance.getUsername());
-        setStringOrNull(insert, 2, instance.getLastname());
-        setStringOrNull(insert, 3, instance.getGivenname());
-        setStringOrNull(insert, 4, instance.getMail());
-        insert.executeUpdate();
-    }
-
-    @Override
-    public boolean update(User instance) throws SQLException {
-        if (instance.getId() < 0) return false;
-        setStringOrNull(update, 1, instance.getLastname());
-        setStringOrNull(update, 2, instance.getGivenname());
-        setStringOrNull(update, 3, instance.getMail());
-        update.setInt(4, instance.getId());
-        return update.executeUpdate() > 0;
-    }
-
-    @Override
-    public List<User> list() throws SQLException {
-        List<User> users = new ArrayList<>();
-        try (var result = list.executeQuery()) {
-            while (result.next()) {
-                users.add(mapColumns(result));
-            }
-        }
-        return users;
-    }
-
-    @Override
-    public User find(int id) throws SQLException {
-        find.setInt(1, id);
-        try (var result = find.executeQuery()) {
-            if (result.next()) {
-                return mapColumns(result);
-            } else {
-                return null;
-            }
-        }
-    }
-
-    @Override
-    public Optional<User> findByUsername(String username) throws SQLException {
-        findByUsername.setString(1, username);
-        try (var result = findByUsername.executeQuery()) {
-            if (result.next()) {
-                return Optional.of(mapColumns(result));
-            } else {
-                return Optional.empty();
-            }
-        }
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Fri Nov 06 10:50:32 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +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.postgres;
-
-import de.uapcore.lightpit.dao.Functions;
-import de.uapcore.lightpit.dao.VersionDao;
-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;
-import java.sql.SQLException;
-import java.util.List;
-
-public final class PGVersionDao implements VersionDao {
-
-    private final PreparedStatement insert, update, list, find, findByNode;
-
-    public PGVersionDao(Connection connection) throws SQLException {
-        final var query = "select versionid, project, name, node, ordinal, status from lpit_version";
-
-        list = connection.prepareStatement(query + " where project = ? " +
-                        "order by ordinal desc, lower(name) desc");
-        find = connection.prepareStatement(query + " where versionid = ?");
-        findByNode = connection.prepareStatement(query + " where project = ? and node = ?");
-
-        insert = connection.prepareStatement(
-                "insert into lpit_version (name, node, ordinal, status, project) values (?, ?, ?, ?::version_status, ?)"
-        );
-        update = connection.prepareStatement(
-                "update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status where versionid = ?"
-        );
-    }
-
-    private static Version mapColumns(ResultSet result) throws SQLException {
-        final var version = new Version(result.getInt("versionid"));
-        version.setName(result.getString("name"));
-        version.setNode(result.getString("node"));
-        version.setOrdinal(result.getInt("ordinal"));
-        version.setStatus(VersionStatus.valueOf(result.getString("status")));
-        return version;
-    }
-
-    private static int setFields(PreparedStatement stmt, Version instance) throws SQLException {
-        int column = 0;
-        stmt.setString(++column, instance.getName());
-        stmt.setString(++column, instance.getNode());
-        stmt.setInt(++column, instance.getOrdinal());
-        stmt.setString(++column, instance.getStatus().name());
-        return column;
-    }
-
-    @Override
-    public void save(Version instance, Project project) throws SQLException {
-        int column = setFields(insert, instance);
-        insert.setInt(++column, project.getId());
-        insert.executeUpdate();
-    }
-
-    @Override
-    public boolean update(Version instance) throws SQLException {
-        if (instance.getId() < 0) return false;
-        int column = setFields(update, instance);
-        update.setInt(++column, instance.getId());
-        return update.executeUpdate() > 0;
-    }
-
-    @Override
-    public List<Version> list(Project project) throws SQLException {
-        list.setInt(1, project.getId());
-        return Functions.list(list, PGVersionDao::mapColumns);
-    }
-
-    @Override
-    public Version find(int id) throws SQLException {
-        find.setInt(1, id);
-        return Functions.find(find, PGVersionDao::mapColumns);
-    }
-
-    @Override
-    public Version findByNode(Project project, String node) throws SQLException {
-        findByNode.setInt(1, project.getId());
-        findByNode.setString(2, node);;
-        return Functions.find(findByNode, PGVersionDao::mapColumns);
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Fri Nov 06 10:50:32 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Thu Nov 19 13:58:54 2020 +0100
@@ -30,7 +30,7 @@
 
 
 import de.uapcore.lightpit.*;
-import de.uapcore.lightpit.dao.DataAccessObjects;
+import de.uapcore.lightpit.dao.DaoProvider;
 import de.uapcore.lightpit.entities.*;
 import de.uapcore.lightpit.types.WebColor;
 import de.uapcore.lightpit.viewmodel.*;
@@ -72,7 +72,7 @@
         }
     }
 
-    private void populate(ProjectView viewModel, PathParameters pathParameters, DataAccessObjects dao) throws SQLException {
+    private void populate(ProjectView viewModel, PathParameters pathParameters, DaoProvider dao) throws SQLException {
         final var projectDao = dao.getProjectDao();
         final var versionDao = dao.getVersionDao();
         final var componentDao = dao.getComponentDao();
@@ -97,7 +97,7 @@
         final var versionNode = pathParameters.get("version");
         if ("no-version".equals(versionNode)) {
             viewModel.setVersionFilter(ProjectView.NO_VERSION);
-        } else if ("all-versions".equals(versionNode)) {
+        } else if ("all-versions".equals(versionNode) || versionNode == null) {
             viewModel.setVersionFilter(ProjectView.ALL_VERSIONS);
         } else {
             viewModel.setVersionFilter(versionDao.findByNode(project, versionNode));
@@ -107,7 +107,7 @@
         final var componentNode = pathParameters.get("component");
         if ("no-component".equals(componentNode)) {
             viewModel.setComponentFilter(ProjectView.NO_COMPONENT);
-        } else if ("all-components".equals(componentNode)) {
+        } else if ("all-components".equals(componentNode) || componentNode == null) {
             viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS);
         } else {
             viewModel.setComponentFilter(componentDao.findByNode(project, componentNode));
@@ -133,7 +133,7 @@
     }
 
     @RequestMapping(method = HttpMethod.GET)
-    public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, ServletException, IOException {
+    public void index(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
         final var viewModel = new ProjectView();
         populate(viewModel, null, dao);
 
@@ -148,13 +148,13 @@
         forwardView(req, resp, viewModel, "projects");
     }
 
-    private void configureProjectEditor(ProjectEditView viewModel, Project project, DataAccessObjects dao) throws SQLException {
+    private void configureProjectEditor(ProjectEditView viewModel, Project project, DaoProvider dao) throws SQLException {
         viewModel.setProject(project);
         viewModel.setUsers(dao.getUserDao().list());
     }
 
     @RequestMapping(requestPath = "$project/edit", method = HttpMethod.GET)
-    public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new ProjectEditView();
         populate(viewModel, pathParams, dao);
 
@@ -168,7 +168,7 @@
     }
 
     @RequestMapping(requestPath = "create", method = HttpMethod.GET)
-    public void create(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, ServletException, IOException {
+    public void create(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
         final var viewModel = new ProjectEditView();
         populate(viewModel, null, dao);
         configureProjectEditor(viewModel, new Project(-1), dao);
@@ -176,7 +176,7 @@
     }
 
     @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
-    public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, ServletException {
+    public void commit(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
 
         try {
             final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
@@ -191,21 +191,27 @@
                     ownerId -> ownerId >= 0 ? new User(ownerId) : null
             ).ifPresent(project::setOwner);
 
-            dao.getProjectDao().saveOrUpdate(project);
+            final var projectDao = dao.getProjectDao();
+            if (project.getId() > 0) {
+                // TODO: unused return value
+                projectDao.update(project);
+            } else {
+                projectDao.save(project);
+            }
 
             setRedirectLocation(req, "./projects/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
             LOG.debug("Successfully updated project {}", project.getName());
 
             renderSite(req, resp);
-        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
+        } catch (NoSuchElementException | IllegalArgumentException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
             // TODO: implement - fix issue #21
         }
     }
 
     @RequestMapping(requestPath = "$project/$component/$version/issues/", method = HttpMethod.GET)
-    public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObjects dao) throws SQLException, IOException, ServletException {
+    public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DaoProvider dao) throws SQLException, IOException, ServletException {
         final var viewModel = new ProjectDetailsView();
         populate(viewModel, pathParams, dao);
 
@@ -263,7 +269,7 @@
     }
 
     @RequestMapping(requestPath = "$project/versions/", method = HttpMethod.GET)
-    public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new VersionsView();
         populate(viewModel, pathParameters, dao);
 
@@ -282,7 +288,7 @@
     }
 
     @RequestMapping(requestPath = "$project/versions/$version/edit", method = HttpMethod.GET)
-    public void editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new VersionEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -297,7 +303,7 @@
     }
 
     @RequestMapping(requestPath = "$project/create-version", method = HttpMethod.GET)
-    public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new VersionEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -312,7 +318,7 @@
     }
 
     @RequestMapping(requestPath = "commit-version", method = HttpMethod.POST)
-    public void commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, ServletException {
+    public void commitVersion(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
 
         try {
             final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
@@ -329,20 +335,27 @@
 
             getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
             version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
-            dao.getVersionDao().saveOrUpdate(version, project);
+
+            final var versionDao = dao.getVersionDao();
+            if (version.getId() > 0) {
+                // TODO: use return value
+                versionDao.update(version);
+            } else {
+                versionDao.save(version, project);
+            }
 
             setRedirectLocation(req, "./projects/" + project.getNode() + "/versions/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             renderSite(req, resp);
-        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
+        } catch (NoSuchElementException | IllegalArgumentException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
             // TODO: implement - fix issue #21
         }
     }
 
     @RequestMapping(requestPath = "$project/components/", method = HttpMethod.GET)
-    public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new ComponentsView();
         populate(viewModel, pathParameters, dao);
 
@@ -360,7 +373,7 @@
     }
 
     @RequestMapping(requestPath = "$project/components/$component/edit", method = HttpMethod.GET)
-    public void editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new ComponentEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -376,7 +389,7 @@
     }
 
     @RequestMapping(requestPath = "$project/create-component", method = HttpMethod.GET)
-    public void createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new ComponentEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -392,7 +405,7 @@
     }
 
     @RequestMapping(requestPath = "commit-component", method = HttpMethod.POST)
-    public void commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, ServletException {
+    public void commitComponent(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
 
         try {
             final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
@@ -414,19 +427,25 @@
             ).ifPresent(component::setLead);
             getParameter(req, String.class, "description").ifPresent(component::setDescription);
 
-            dao.getComponentDao().saveOrUpdate(component, project);
+            final var componentDao = dao.getComponentDao();
+            if (component.getId() > 0) {
+                // TODO: use return value
+                componentDao.update(component);
+            } else {
+                componentDao.save(component, project);
+            }
 
             setRedirectLocation(req, "./projects/" + project.getNode() + "/components/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             renderSite(req, resp);
-        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
+        } catch (NoSuchElementException | IllegalArgumentException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
             // TODO: implement - fix issue #21
         }
     }
 
-    private void configureIssueEditor(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
+    private void configureIssueEditor(IssueEditView viewModel, Issue issue, DaoProvider dao) throws SQLException {
         final var project = viewModel.getProjectInfo().getProject();
         issue.setProject(project); // automatically set current project for new issues
         viewModel.setIssue(issue);
@@ -436,7 +455,7 @@
     }
 
     @RequestMapping(requestPath = "$project/issues/$issue/view", method = HttpMethod.GET)
-    public void viewIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void viewIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new IssueDetailView();
         populate(viewModel, pathParameters, dao);
 
@@ -462,7 +481,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, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void editIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new IssueEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -486,7 +505,7 @@
     }
 
     @RequestMapping(requestPath = "$project/create-issue", method = HttpMethod.GET)
-    public void createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException, ServletException {
+    public void createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DaoProvider dao) throws IOException, SQLException, ServletException {
         final var viewModel = new IssueEditView();
         populate(viewModel, pathParameters, dao);
 
@@ -504,7 +523,7 @@
     }
 
     @RequestMapping(requestPath = "commit-issue", method = HttpMethod.POST)
-    public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, ServletException {
+    public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
         try {
             final var issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
             final var componentId = getParameter(req, Integer.class, "component");
@@ -549,21 +568,27 @@
                             stream.map(Version::new).collect(Collectors.toList())
                     ).ifPresent(issue::setResolvedVersions);
 
-            dao.getIssueDao().saveOrUpdate(issue, issue.getProject());
+            final var issueDao = dao.getIssueDao();
+            if (issue.getId() > 0) {
+                // TODO: use return value
+                issueDao.update(issue);
+            } else {
+                issueDao.save(issue, project);
+            }
 
             // TODO: fix redirect location
             setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/view");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             renderSite(req, resp);
-        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
+        } catch (NoSuchElementException | IllegalArgumentException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
             // TODO: implement - fix issue #21
         }
     }
 
     @RequestMapping(requestPath = "commit-issue-comment", method = HttpMethod.POST)
-    public void commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException, ServletException {
+    public void commentIssue(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws IOException, ServletException {
         final var issueIdParam = getParameter(req, Integer.class, "issueid");
         if (issueIdParam.isEmpty()) {
             resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form.");
@@ -584,7 +609,7 @@
 
             LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId());
             if (req.getRemoteUser() != null) {
-                dao.getUserDao().findByUsername(req.getRemoteUser()).ifPresent(issueComment::setAuthor);
+                Optional.ofNullable(dao.getUserDao().findByUsername(req.getRemoteUser())).ifPresent(issueComment::setAuthor);
             }
 
             dao.getIssueDao().saveComment(issue, issueComment);
@@ -594,7 +619,7 @@
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             renderSite(req, resp);
-        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
+        } catch (NoSuchElementException | IllegalArgumentException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
             // TODO: implement - fix issue #21
         }
--- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Fri Nov 06 10:50:32 2020 +0100
+++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Thu Nov 19 13:58:54 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.DataAccessObjects;
+import de.uapcore.lightpit.dao.DaoProvider;
 import de.uapcore.lightpit.entities.User;
 import de.uapcore.lightpit.viewmodel.UsersEditView;
 import de.uapcore.lightpit.viewmodel.UsersView;
@@ -61,7 +61,7 @@
     }
 
     @RequestMapping(method = HttpMethod.GET)
-    public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, ServletException, IOException {
+    public void index(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
         final var userDao = dao.getUserDao();
 
         final var viewModel = new UsersView();
@@ -73,7 +73,7 @@
     }
 
     @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
-    public void edit(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, ServletException, IOException {
+    public void edit(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws SQLException, ServletException, IOException {
 
         final var viewModel = new UsersEditView();
         viewModel.setUser(findByParameter(req, Integer.class, "id",
@@ -86,7 +86,7 @@
     }
 
     @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
-    public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws ServletException, IOException {
+    public void commit(HttpServletRequest req, HttpServletResponse resp, DaoProvider dao) throws ServletException, IOException {
 
         User user = new User(-1);
         try {
@@ -96,13 +96,19 @@
             getParameter(req, String.class, "lastname").ifPresent(user::setLastname);
             getParameter(req, String.class, "mail").ifPresent(user::setMail);
 
-            dao.getUserDao().saveOrUpdate(user);
+            final var userDao = dao.getUserDao();
+            if (user.getId() > 0) {
+                // TODO: unused return value
+                userDao.update(user);
+            } else {
+                userDao.save(user);
+            }
 
             setRedirectLocation(req, "./teams/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             LOG.debug("Successfully updated user {}", user.getUsername());
-        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
+        } catch (NoSuchElementException | IllegalArgumentException ex) {
             final var viewModel = new UsersEditView();
             viewModel.setUser(user);
             // TODO: viewModel.setErrorText()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractChildEntityDao.kt	Thu Nov 19 13:58:54 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
+
+
+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
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractComponentDao.kt	Thu Nov 19 13:58:54 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.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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractDao.kt	Thu Nov 19 13:58:54 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 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractEntityDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,61 @@
+/*
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractIssueDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,99 @@
+/*
+ * 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.
+     *
+     * @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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractProjectDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,37 @@
+/*
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractUserDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,33 @@
+/*
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/AbstractVersionDao.kt	Thu Nov 19 13:58:54 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.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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DaoProvider.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,35 @@
+/*
+ * 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/postgres/PGComponentDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,110 @@
+/*
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGDaoProvider.kt	Thu Nov 19 13:58:54 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.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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGIssueDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,249 @@
+/*
+ * 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"
+    )
+
+    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()
+        }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGProjectDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,122 @@
+/*
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGUserDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,95 @@
+/*
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/postgres/PGVersionDao.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -0,0 +1,105 @@
+/*
+ * 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/User.kt	Fri Nov 06 10:50:32 2020 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/User.kt	Thu Nov 19 13:58:54 2020 +0100
@@ -26,7 +26,7 @@
 package de.uapcore.lightpit.entities
 
 data class User(val id: Int) {
-    var username = "anonymous"
+    var username = ""
     var mail = ""
     var givenname = ""
     var lastname = ""

mercurial