adds custom node names - fixes #27

Thu, 22 Oct 2020 13:03:26 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 22 Oct 2020 13:03:26 +0200
changeset 138
e2aa673dd473
parent 137
a7e543ab0c5f
child 139
6abc75d213ef

adds custom node names - fixes #27

setup/postgres/psql_create_tables.sql 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/Functions.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/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/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/PGVersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Component.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Project.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Version.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/component-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
--- a/setup/postgres/psql_create_tables.sql	Thu Oct 22 12:00:34 2020 +0200
+++ b/setup/postgres/psql_create_tables.sql	Thu Oct 22 13:03:26 2020 +0200
@@ -12,6 +12,7 @@
 create table lpit_project (
     projectid       serial          primary key,
     name            varchar(20)     not null unique,
+    node            varchar(20)     not null unique,
     description     varchar(200),
     repoUrl         varchar(50),
     owner           integer         references lpit_user(userid)
@@ -29,21 +30,26 @@
     versionid       serial          primary key,
     project         integer         not null references lpit_project(projectid),
     name            varchar(20)     not null,
+    node            varchar(20)     not null,
     ordinal         integer         not null default 0,
     status          version_status  not null default 'Future'
 );
 
+create unique index lpit_version_node_unique on lpit_version(project, node);
 
 create table lpit_component (
     id              serial          primary key,
     project         integer         not null references lpit_project(projectid),
     name            varchar(20)     not null,
+    node            varchar(20)     not null,
     color           char(6)         not null default '000000',
     ordinal         integer         not null default 0,
     description     text,
     lead            integer         references lpit_user(userid)
 );
 
+create unique index lpit_component_node_unique on lpit_component(project, node);
+
 create type issue_status as enum (
     'InSpecification',
     'ToDo',
--- a/src/main/java/de/uapcore/lightpit/dao/ComponentDao.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/ComponentDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -31,6 +31,8 @@
 import de.uapcore.lightpit.entities.Component;
 import de.uapcore.lightpit.entities.Project;
 
-public interface ComponentDao extends ChildEntityDao<Component, 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/Functions.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/Functions.java	Thu Oct 22 13:03:26 2020 +0200
@@ -28,10 +28,9 @@
  */
 package de.uapcore.lightpit.dao;
 
-import java.sql.Date;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.sql.Types;
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
 
@@ -65,6 +64,33 @@
         }
     }
 
+    @FunctionalInterface
+    public interface ResultSetMapper<T> {
+        T apply(ResultSet rs) throws SQLException;
+    }
+
+    public static <T> List<T> list(PreparedStatement stmt, ResultSetMapper<T> mapper) throws SQLException {
+        List<T> results = new ArrayList<>();
+        try (var result = stmt.executeQuery()) {
+            while (result.next()) {
+                final var project = mapper.apply(result);
+                results.add(project);
+            }
+        }
+        return results;
+    }
+
+    public static <T> T find(PreparedStatement stmt, ResultSetMapper<T> mapper) throws SQLException {
+        try (var result = stmt.executeQuery()) {
+            if (result.next()) {
+                final var ent = mapper.apply(result);
+                return ent;
+            } else {
+                return null;
+            }
+        }
+    }
+
     private Functions() {
 
     }
--- a/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -35,4 +35,6 @@
 
 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/VersionDao.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -31,5 +31,8 @@
 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	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGComponentDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -39,42 +39,38 @@
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 
 public final class PGComponentDao implements ComponentDao {
 
-    private final PreparedStatement insert, update, list, find;
+    private final PreparedStatement insert, update, list, find, findByNode;
 
     public PGComponentDao(Connection connection) throws SQLException {
-        list = connection.prepareStatement(
-                "select id, name, color, ordinal, description, " +
-                        "userid, username, givenname, lastname, mail " +
-                        "from lpit_component " +
-                        "left join lpit_user on lead = userid " +
-                        "where project = ? " +
+        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 desc, lower(name) desc");
 
-        find = connection.prepareStatement(
-                "select id, name, color, ordinal, description, " +
-                        "userid, username, givenname, lastname, mail " +
-                        "from lpit_component " +
-                        "left join lpit_user on lead = userid " +
-                        "where id = ? ");
+        find = connection.prepareStatement(query + " where id = ? ");
+
+        findByNode = connection.prepareStatement(query + " where project = ? and node = ?");
 
         insert = connection.prepareStatement(
-                "insert into lpit_component (project, name, color, ordinal, description, lead) values (?, ?, ?, ?, ?, ?)"
+                "insert into lpit_component (name, node, color, ordinal, description, lead, project) values (?, ?, ?, ?, ?, ?, ?)"
         );
 
         update = connection.prepareStatement(
-                "update lpit_component set name = ?, color = ?, ordinal = ?, description = ?, lead = ? where id = ?"
+                "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) {
@@ -87,53 +83,49 @@
         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 {
-        Objects.requireNonNull(instance.getName());
-        insert.setInt(1, project.getId());
-        insert.setString(2, instance.getName());
-        insert.setString(3, instance.getColor().getHex());
-        insert.setInt(4, instance.getOrdinal());
-        Functions.setStringOrNull(insert, 5, instance.getDescription());
-        Functions.setForeignKeyOrNull(insert, 6, instance.getLead(), User::getId);
+        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;
-        Objects.requireNonNull(instance.getName());
-        Objects.requireNonNull(instance.getColor());
-        update.setString(1, instance.getName());
-        update.setString(2, instance.getColor().getHex());
-        update.setInt(3, instance.getOrdinal());
-        Functions.setStringOrNull(update, 4, instance.getDescription());
-        Functions.setForeignKeyOrNull(update, 5, instance.getLead(), User::getId);
-        update.setInt(6, instance.getId());
+        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());
-        List<Component> components = new ArrayList<>();
-        try (var result = list.executeQuery()) {
-            while (result.next()) {
-                components.add(mapColumns(result));
-            }
-        }
-        return components;
+        return Functions.list(list, PGComponentDao::mapColumns);
     }
 
     @Override
     public Component find(int id) throws SQLException {
         find.setInt(1, id);
-        try (var result = find.executeQuery()) {
-            if (result.next()) {
-                return mapColumns(result);
-            } else {
-                return null;
-            }
-        }
+        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/PGIssueDao.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -48,43 +48,29 @@
     private final PreparedStatement insertComment, updateComment, listComments;
 
     public PGIssueDao(Connection connection) throws SQLException {
-        list = connection.prepareStatement(
-                "select issueid, i.project, p.name as projectname, component, c.name as componentname, " +
+        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 " +
+                        "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) "+
-                        "select issueid, i.project, p.name as projectname, component, c.name as componentname, " +
-                        "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 " +
+                        query +
                         "left join issue_version using (issueid) "+
-                        "left join lpit_user on userid = assignee " +
                         "where coalesce(versionid,-1) = ? and coalesce(component, -1) = coalesce(?, component, -1)"
         );
 
-        find = connection.prepareStatement(
-                "select issueid, i.project, p.name as projectname, component, c.name as componentname, " +
-                        "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 " +
-                        "where issueid = ? ");
+        find = connection.prepareStatement(query + "where issueid = ? ");
 
         insert = connection.prepareStatement(
                 "insert into lpit_issue (project, component, status, category, subject, description, assignee, eta) " +
@@ -128,11 +114,13 @@
     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);
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -28,6 +28,7 @@
  */
 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;
@@ -37,32 +38,26 @@
 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 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;
+    private final PreparedStatement insert, update, list, find, findByNode;
     private final PreparedStatement issue_summary;
 
     public PGProjectDao(Connection connection) throws SQLException {
-        list = connection.prepareStatement(
-                "select projectid, name, description, repourl, " +
-                        "userid, username, lastname, givenname, mail " +
-                        "from lpit_project " +
-                        "left join lpit_user owner on lpit_project.owner = owner.userid " +
-                        "order by name");
+        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 ";
 
-        find = connection.prepareStatement(
-                "select projectid, name, description, repourl, " +
-                        "userid, username, lastname, givenname, mail " +
-                        "from lpit_project " +
-                        "left join lpit_user owner on lpit_project.owner = owner.userid " +
-                        "where projectid = ?");
+        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 "+
@@ -73,16 +68,17 @@
         );
 
         insert = connection.prepareStatement(
-                "insert into lpit_project (name, description, repourl, owner) values (?, ?, ?, ?)"
+                "insert into lpit_project (name, node, description, repourl, owner) values (?, ?, ?, ?, ?)"
         );
         update = connection.prepareStatement(
-                "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where projectid = ?"
+                "update lpit_project set name = ?, node = ?, description = ?, repourl = ?, owner = ? where projectid = ?"
         );
     }
 
-    public Project mapColumns(ResultSet result) throws SQLException {
+    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));
@@ -112,50 +108,44 @@
         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 {
-        Objects.requireNonNull(instance.getName());
-        insert.setString(1, instance.getName());
-        setStringOrNull(insert, 2, instance.getDescription());
-        setStringOrNull(insert, 3, instance.getRepoUrl());
-        setForeignKeyOrNull(insert, 4, instance.getOwner(), User::getId);
+        setColumns(insert, instance);
         insert.executeUpdate();
     }
 
     @Override
     public boolean update(Project instance) throws SQLException {
         if (instance.getId() < 0) return false;
-        Objects.requireNonNull(instance.getName());
-        update.setString(1, instance.getName());
-        setStringOrNull(update, 2, instance.getDescription());
-        setStringOrNull(update, 3, instance.getRepoUrl());
-        setForeignKeyOrNull(update, 4, instance.getOwner(), User::getId);
-        update.setInt(5, instance.getId());
+        int column = setColumns(update, instance);
+        update.setInt(++column, instance.getId());
         return update.executeUpdate() > 0;
     }
 
     @Override
     public List<Project> list() throws SQLException {
-        List<Project> projects = new ArrayList<>();
-        try (var result = list.executeQuery()) {
-            while (result.next()) {
-                final var project = mapColumns(result);
-                projects.add(project);
-            }
-        }
-        return projects;
+        return Functions.list(list, PGProjectDao::mapColumns);
     }
 
     @Override
     public Project find(int id) throws SQLException {
         find.setInt(1, id);
-        try (var result = find.executeQuery()) {
-            if (result.next()) {
-                final var project = mapColumns(result);
-                return project;
-            } else {
-                return null;
-            }
-        }
+        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/PGVersionDao.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Thu Oct 22 13:03:26 2020 +0200
@@ -28,6 +28,7 @@
  */
 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;
@@ -37,84 +38,77 @@
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 
 public final class PGVersionDao implements VersionDao {
 
-    private final PreparedStatement insert, update, list, find;
+    private final PreparedStatement insert, update, list, find, findByNode;
 
     public PGVersionDao(Connection connection) throws SQLException {
-        list = connection.prepareStatement(
-                "select versionid, project, name, ordinal, status " +
-                        "from lpit_version " +
-                        "where project = ? " +
+        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(
-                "select versionid, project, name, ordinal, status " +
-                        "from lpit_version  " +
-                        "where versionid = ?");
+        find = connection.prepareStatement(query + " where versionid = ?");
+        findByNode = connection.prepareStatement(query + " where project = ? and node = ?");
 
         insert = connection.prepareStatement(
-                "insert into lpit_version (project, name, ordinal, status) values (?, ?, ?, ?::version_status)"
+                "insert into lpit_version (name, node, ordinal, status, project) values (?, ?, ?, ?::version_status, ?)"
         );
         update = connection.prepareStatement(
-                "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?"
+                "update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status where versionid = ?"
         );
     }
 
-    private Version mapColumns(ResultSet result) throws SQLException {
+    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 {
-        Objects.requireNonNull(instance.getName());
-        insert.setInt(1, project.getId());
-        insert.setString(2, instance.getName());
-        insert.setInt(3, instance.getOrdinal());
-        insert.setString(4, instance.getStatus().name());
+        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;
-        Objects.requireNonNull(instance.getName());
-        update.setString(1, instance.getName());
-        update.setInt(2, instance.getOrdinal());
-        update.setString(3, instance.getStatus().name());
-        update.setInt(4, instance.getId());
+        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());
-        List<Version> versions = new ArrayList<>();
-        try (var result = list.executeQuery()) {
-            while (result.next()) {
-                versions.add(mapColumns(result));
-            }
-        }
-        return versions;
+        return Functions.list(list, PGVersionDao::mapColumns);
     }
 
     @Override
     public Version find(int id) throws SQLException {
         find.setInt(1, id);
-        try (var result = find.executeQuery()) {
-            if (result.next()) {
-                return mapColumns(result);
-            } else {
-                return null;
-            }
-        }
+        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/entities/Component.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Component.java	Thu Oct 22 13:03:26 2020 +0200
@@ -69,7 +69,7 @@
     }
 
     public String getNode() {
-        return node == null ? String.valueOf(id) : node;
+        return node;
     }
 
     public void setNode(String node) {
--- a/src/main/java/de/uapcore/lightpit/entities/Project.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Project.java	Thu Oct 22 13:03:26 2020 +0200
@@ -56,7 +56,7 @@
     }
 
     public String getNode() {
-        return node == null ? String.valueOf(id) : node;
+        return node;
     }
 
     public void setNode(String node) {
--- a/src/main/java/de/uapcore/lightpit/entities/Version.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Version.java	Thu Oct 22 13:03:26 2020 +0200
@@ -58,7 +58,7 @@
     }
 
     public String getNode() {
-        return node == null ? String.valueOf(id) : node;
+        return node;
     }
 
     public void setNode(String node) {
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Thu Oct 22 13:03:26 2020 +0200
@@ -74,42 +74,44 @@
             return;
 
         // Select Project
-        final int pid = Functions.parseIntOrZero(pathParameters.get("project"));
-        if (pid > 0) {
-            final var project = projectDao.find(pid);
-            if (project != null) {
-                final var info = new ProjectInfo(project);
-                info.setVersions(versionDao.list(project));
-                info.setComponents(componentDao.list(project));
-                info.setIssueSummary(projectDao.getIssueSummary(project));
-                viewModel.setProjectInfo(info);
-            }
-        }
+        final var project = projectDao.findByNode(pathParameters.get("project"));
+        if (project == null)
+            return;
+
+        final var info = new ProjectInfo(project);
+        info.setVersions(versionDao.list(project));
+        info.setComponents(componentDao.list(project));
+        info.setIssueSummary(projectDao.getIssueSummary(project));
+        viewModel.setProjectInfo(info);
 
         // Select Version
-        final var pathParamVersion = pathParameters.get("version");
-        if ("no-version".equals(pathParamVersion)) {
+        final var versionNode = pathParameters.get("version");
+        if ("no-version".equals(versionNode)) {
             viewModel.setVersionFilter(ProjectView.NO_VERSION);
-        } else if ("all-versions".equals(pathParamVersion)) {
+        } else if ("all-versions".equals(versionNode)) {
             viewModel.setVersionFilter(ProjectView.ALL_VERSIONS);
         } else {
-            final int vid = Functions.parseIntOrZero(pathParamVersion);
-            if (vid > 0) {
-                viewModel.setVersionFilter(versionDao.find(vid));
-            }
+            viewModel.setVersionFilter(versionDao.findByNode(project, versionNode));
         }
 
         // Select Component
-        final var pathParamComponent = pathParameters.get("component");
-        if ("no-component".equals(pathParamComponent)) {
+        final var componentNode = pathParameters.get("component");
+        if ("no-component".equals(componentNode)) {
             viewModel.setComponentFilter(ProjectView.NO_COMPONENT);
-        } else if ("all-components".equals(pathParamComponent)) {
+        } else if ("all-components".equals(componentNode)) {
             viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS);
         } else {
-            final int cid = Functions.parseIntOrZero(pathParamComponent);
-            if (cid > 0) {
-                viewModel.setComponentFilter(componentDao.find(cid));
-            }
+            viewModel.setComponentFilter(componentDao.findByNode(project, componentNode));
+        }
+    }
+
+    private static String sanitizeNode(String node, String defaultValue) {
+        String result = node == null || node.isBlank() ? defaultValue : node;
+        result = result.replace('/', '-');
+        if (result.equals(".") || result.equals("..")) {
+            return "_"+result;
+        } else {
+            return result;
         }
     }
 
@@ -170,6 +172,10 @@
         try {
             final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
             project.setName(getParameter(req, String.class, "name").orElseThrow());
+
+            final var node = getParameter(req, String.class, "node").orElse(null);
+            project.setNode(sanitizeNode(node, project.getName()));
+
             getParameter(req, String.class, "description").ifPresent(project::setDescription);
             getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
             getParameter(req, Integer.class, "owner").map(
@@ -301,14 +307,23 @@
     public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
 
         try {
-            final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
+            final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
+            if (project == null) {
+                // TODO: improve error handling, because not found is not correct for this POST request
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+                return ResponseType.NONE;
+            }
             final var version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
             version.setName(getParameter(req, String.class, "name").orElseThrow());
+
+            final var node = getParameter(req, String.class, "node").orElse(null);
+            version.setNode(sanitizeNode(node, version.getName()));
+
             getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
             version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
             dao.getVersionDao().saveOrUpdate(version, project);
 
-            setRedirectLocation(req, "./projects/" + project.getId() + "/versions/");
+            setRedirectLocation(req, "./projects/" + project.getNode() + "/versions/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
@@ -373,9 +388,18 @@
     public ResponseType commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
 
         try {
-            final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
+            final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
+            if (project == null) {
+                // TODO: improve error handling, because not found is not correct for this POST request
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+                return ResponseType.NONE;
+            }
             final var component = new Component(getParameter(req, Integer.class, "id").orElseThrow());
             component.setName(getParameter(req, String.class, "name").orElseThrow());
+
+            final var node = getParameter(req, String.class, "node").orElse(null);
+            component.setNode(sanitizeNode(node, component.getName()));
+
             component.setColor(getParameter(req, WebColor.class, "color").orElseThrow());
             getParameter(req, Integer.class, "ordinal").ifPresent(component::setOrdinal);
             getParameter(req, Integer.class, "lead").map(
@@ -385,7 +409,7 @@
 
             dao.getComponentDao().saveOrUpdate(component, project);
 
-            setRedirectLocation(req, "./projects/" + project.getId() + "/components/");
+            setRedirectLocation(req, "./projects/" + project.getNode() + "/components/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
@@ -461,7 +485,13 @@
             } else {
                 component = null;
             }
-            issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
+            final var project = dao.getProjectDao().find(getParameter(req, Integer.class, "pid").orElseThrow());
+            if (project == null) {
+                // TODO: improve error handling, because not found is not correct for this POST request
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+                return ResponseType.NONE;
+            }
+            issue.setProject(project);
             getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
             getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
             issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
@@ -493,7 +523,7 @@
             dao.getIssueDao().saveOrUpdate(issue, issue.getProject());
 
             // TODO: fix issue #14
-            setRedirectLocation(req, "./projects/" + issue.getProject().getId() + "/all-components/all-versions/issues/");
+            setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/all-components/all-versions/issues/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             return ResponseType.HTML;
@@ -532,7 +562,7 @@
             dao.getIssueDao().saveComment(issueComment);
 
             // TODO: fix redirect location (e.g. after fixing #24)
-            setRedirectLocation(req, "./projects/" + issue.getProject().getId()+"/issues/"+issue.getId()+"/edit");
+            setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/edit");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             return ResponseType.HTML;
--- a/src/main/resources/localization/projects.properties	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/resources/localization/projects.properties	Thu Oct 22 13:03:26 2020 +0200
@@ -40,6 +40,8 @@
 navmenu.all=all
 
 name=Name
+node=Node
+node.tooltip=Name of the path node that will be used in URL construction.
 description=Description
 repoUrl=Repository
 owner=Project Lead
--- a/src/main/resources/localization/projects_de.properties	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/resources/localization/projects_de.properties	Thu Oct 22 13:03:26 2020 +0200
@@ -40,6 +40,8 @@
 navmenu.all=Alle
 
 name=Name
+node=Pfadname
+node.tooltip=Name, der zur Konstruktion der URL genutzt werden soll.
 description=Beschreibung
 repoUrl=Repository
 owner=Projektleitung
--- a/src/main/webapp/WEB-INF/jsp/component-form.jsp	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/component-form.jsp	Thu Oct 22 13:03:26 2020 +0200
@@ -50,6 +50,10 @@
             <th><fmt:message key="component.name"/></th>
             <td><input name="name" type="text" maxlength="20" required value="<c:out value="${component.name}"/>" /></td>
         </tr>
+        <tr title="<fmt:message key="node.tooltip"/>">
+            <th><fmt:message key="node"/></th>
+            <td><input name="node" type="text" maxlength="20" required value="<c:out value="${component.node}"/>" /></td>
+        </tr>
         <tr>
             <th><fmt:message key="component.color"/></th>
             <td><input name="color" type="color" required value="${component.color}" /></td>
--- a/src/main/webapp/WEB-INF/jsp/project-form.jsp	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp	Thu Oct 22 13:03:26 2020 +0200
@@ -42,6 +42,10 @@
             <th><fmt:message key="name"/></th>
             <td><input name="name" type="text" maxlength="20" required value="<c:out value="${project.name}"/>" /></td>
         </tr>
+        <tr title="<fmt:message key="node.tooltip"/>">
+            <th><fmt:message key="node"/></th>
+            <td><input name="node" type="text" maxlength="20" required value="<c:out value="${project.node}"/>" /></td>
+        </tr>
         <tr>
             <th><fmt:message key="description"/></th>
             <td><input type="text" name="description" maxlength="200" value="<c:out value="${project.description}"/>" /></td>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Thu Oct 22 12:00:34 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Thu Oct 22 13:03:26 2020 +0200
@@ -50,6 +50,10 @@
             <th><fmt:message key="version.name"/></th>
             <td><input name="name" type="text" maxlength="20" required value="<c:out value="${version.name}"/>" /></td>
         </tr>
+        <tr title="<fmt:message key="node.tooltip"/>">
+            <th><fmt:message key="node"/></th>
+            <td><input name="node" type="text" maxlength="20" required value="<c:out value="${version.node}"/>" /></td>
+        </tr>
         <tr>
             <th><fmt:message key="version.status"/></th>
             <td>

mercurial