changes request mapping to contain project and version ID as path parameters (this removes the session storage)

Thu, 15 Oct 2020 20:02:30 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 15 Oct 2020 20:02:30 +0200
changeset 131
67df332e3146
parent 130
7ef369744fd1
child 132
57e5a4624919

changes request mapping to contain project and version ID as path parameters (this removes the session storage)

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/Functions.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-navmenu.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/projects.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/versions.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jspf file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Thu Oct 15 20:02:30 2020 +0200
@@ -194,12 +194,15 @@
                         try {
                             PathPattern pathPattern = new PathPattern(mapping.get().requestPath());
 
-                            if (mappings
-                                    .computeIfAbsent(mapping.get().method(), k -> new HashMap<>())
-                                    .putIfAbsent(pathPattern, method) != null) {
-                                LOG.warn("{} {} has multiple mappings",
+                            final var methodMappings = mappings.computeIfAbsent(mapping.get().method(), k -> new HashMap<>());
+                            final var currentMapping = methodMappings.putIfAbsent(pathPattern, method);
+                            if (currentMapping != null) {
+                                LOG.warn("Cannot map {} {} to {} in class {} - this would override the mapping to {}",
                                         mapping.get().method(),
-                                        mapping.get().requestPath()
+                                        mapping.get().requestPath(),
+                                        method.getName(),
+                                        getClass().getSimpleName(),
+                                        currentMapping.getName()
                                 );
                             }
 
@@ -296,6 +299,32 @@
         req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel);
     }
 
+    private <T> Optional<T> parseParameter(String paramValue, Class<T> clazz) {
+        if (paramValue == null) return Optional.empty();
+        if (clazz.equals(Boolean.class)) {
+            if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) {
+                return Optional.of((T) Boolean.FALSE);
+            } else {
+                return Optional.of((T) Boolean.TRUE);
+            }
+        }
+        if (clazz.equals(String.class)) return Optional.of((T) paramValue);
+        if (java.sql.Date.class.isAssignableFrom(clazz)) {
+            try {
+                return Optional.of((T) java.sql.Date.valueOf(paramValue));
+            } catch (IllegalArgumentException ex) {
+                return Optional.empty();
+            }
+        }
+        try {
+            final Constructor<T> ctor = clazz.getConstructor(String.class);
+            return Optional.of(ctor.newInstance(paramValue));
+        } catch (ReflectiveOperationException e) {
+            // does not type check and is not convertible - treat as if the parameter was never set
+            return Optional.empty();
+        }
+    }
+
     /**
      * Obtains a request parameter of the specified type.
      * The specified type must have a single-argument constructor accepting a string to perform conversion.
@@ -322,30 +351,7 @@
             }
             return Optional.of(array);
         } else {
-            final String paramValue = req.getParameter(name);
-            if (paramValue == null) return Optional.empty();
-            if (clazz.equals(Boolean.class)) {
-                if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) {
-                    return Optional.of((T) Boolean.FALSE);
-                } else {
-                    return Optional.of((T) Boolean.TRUE);
-                }
-            }
-            if (clazz.equals(String.class)) return Optional.of((T) paramValue);
-            if (java.sql.Date.class.isAssignableFrom(clazz)) {
-                try {
-                    return Optional.of((T) java.sql.Date.valueOf(paramValue));
-                } catch (IllegalArgumentException ex) {
-                    return Optional.empty();
-                }
-            }
-            try {
-                final Constructor<T> ctor = clazz.getConstructor(String.class);
-                return Optional.of(ctor.newInstance(paramValue));
-            } catch (ReflectiveOperationException e) {
-                // does not type check and is not convertible - treat as if the parameter was never set
-                return Optional.empty();
-            }
+            return parseParameter(req.getParameter(name), clazz);
         }
     }
 
--- a/src/main/java/de/uapcore/lightpit/Functions.java	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/Functions.java	Thu Oct 15 20:02:30 2020 +0200
@@ -74,6 +74,14 @@
         return req.getServletPath() + Optional.ofNullable(req.getPathInfo()).orElse("");
     }
 
+    public static int parseIntOrZero(String str) {
+        try {
+            return Integer.parseInt(str);
+        } catch (NumberFormatException ex) {
+            return 0;
+        }
+    }
+
     /**
      * This class is not instantiatable.
      */
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Thu Oct 15 20:02:30 2020 +0200
@@ -44,12 +44,9 @@
 import java.sql.Date;
 import java.sql.SQLException;
 import java.util.NoSuchElementException;
-import java.util.Optional;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import static de.uapcore.lightpit.Functions.fqn;
-
 @WebServlet(
         name = "ProjectsModule",
         urlPatterns = "/projects/*"
@@ -58,45 +55,26 @@
 
     private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
 
-    private static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
-    private static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
-    private static final String SESSION_ATTR_SELECTED_COMPONENT = fqn(ProjectsModule.class, "selected_component");
-    private static final String PARAMETER_SELECTED_PROJECT = "pid";
-    private static final String PARAMETER_SELECTED_VERSION = "vid";
-    private static final String PARAMETER_SELECTED_COMPONENT = "cid";
-
     @Override
     protected String getResourceBundleName() {
         return "localization.projects";
     }
 
-    private int syncParamWithSession(HttpServletRequest req, String param, String attr) {
-        final var session = req.getSession();
-        final var idParam = getParameter(req, Integer.class, param);
-        final int id;
-        if (idParam.isPresent()) {
-            id = idParam.get();
-            session.setAttribute(attr, id);
-        } else {
-            id = Optional.ofNullable(session.getAttribute(attr)).map(x->(Integer)x).orElse(-1);
-        }
-        return id;
-    }
-
-    private void populate(ProjectView viewModel, HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+    private void populate(ProjectView viewModel, PathParameters pathParameters, DataAccessObjects dao) throws SQLException {
         final var projectDao = dao.getProjectDao();
         final var versionDao = dao.getVersionDao();
         final var componentDao = dao.getComponentDao();
 
         projectDao.list().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add);
 
+        if (pathParameters == null)
+            return;
+
         // Select Project
-        final int pid = syncParamWithSession(req, PARAMETER_SELECTED_PROJECT, SESSION_ATTR_SELECTED_PROJECT);
-        if (pid >= 0) {
+        final int pid = Functions.parseIntOrZero(pathParameters.get("project"));
+        if (pid > 0) {
             final var project = projectDao.find(pid);
-            if (project == null) {
-                req.setAttribute(SESSION_ATTR_SELECTED_PROJECT, -1);
-            } else {
+            if (project != null) {
                 final var info = new ProjectInfo(project);
                 info.setVersions(versionDao.list(project));
                 info.setComponents(componentDao.list(project));
@@ -106,22 +84,19 @@
         }
 
         // Select Version
-        final int vid = syncParamWithSession(req, PARAMETER_SELECTED_VERSION, SESSION_ATTR_SELECTED_VERSION);
+        final int vid = Functions.parseIntOrZero(pathParameters.get("version"));
         if (vid > 0) {
             viewModel.setVersionFilter(versionDao.find(vid));
-        } else {
-            // NULL for version means: show all unassigned
-            viewModel.setVersionFilter(null);
+        }
+        // TODO: don't treat unknown == unassigned - send 404 for unknown and introduce special word for unassigned
+
+        // Select Component
+        final int cid = Functions.parseIntOrZero(pathParameters.get("component"));
+        if (cid > 0) {
+            viewModel.setComponentFilter(componentDao.find(cid));
         }
 
-        // Select Component
-        final int cid = syncParamWithSession(req, PARAMETER_SELECTED_COMPONENT, SESSION_ATTR_SELECTED_COMPONENT);
-        if (cid > 0) {
-            viewModel.setComponentFilter(componentDao.find(cid));
-        } else if (cid <= 0) {
-            // -1 means: filter for unassigned, null means: show all
-            viewModel.setComponentFilter(new Component(-1));
-        }
+        // TODO: distinguish all/unassigned for components
     }
 
     private ResponseType forwardView(HttpServletRequest req, ProjectView viewModel, String name) {
@@ -135,7 +110,7 @@
     @RequestMapping(method = HttpMethod.GET)
     public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
         final var viewModel = new ProjectView();
-        populate(viewModel, req, dao);
+        populate(viewModel, null, dao);
 
         final var projectDao = dao.getProjectDao();
         final var versionDao = dao.getVersionDao();
@@ -148,30 +123,38 @@
         return forwardView(req, viewModel, "projects");
     }
 
-    private void configure(ProjectEditView viewModel, Project project, DataAccessObjects dao) throws SQLException {
+    private void configureProjectEditor(ProjectEditView viewModel, Project project, DataAccessObjects dao) throws SQLException {
         viewModel.setProject(project);
         viewModel.setUsers(dao.getUserDao().list());
     }
 
-    @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
-    public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+    @RequestMapping(requestPath = "$project/edit", method = HttpMethod.GET)
+    public ResponseType edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObjects dao) throws IOException, SQLException {
         final var viewModel = new ProjectEditView();
-        populate(viewModel, req, dao);
+        populate(viewModel, pathParams, dao);
+
+        if (viewModel.getProjectInfo() == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return ResponseType.NONE;
+        }
 
-        final var project = Optional.ofNullable(viewModel.getProjectInfo())
-                .map(ProjectInfo::getProject)
-                .orElse(new Project(-1));
-        configure(viewModel, project, dao);
+        configureProjectEditor(viewModel, viewModel.getProjectInfo().getProject(), dao);
+        return forwardView(req, viewModel, "project-form");
+    }
 
+    @RequestMapping(requestPath = "create", method = HttpMethod.GET)
+    public ResponseType create(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+        final var viewModel = new ProjectEditView();
+        populate(viewModel, null, dao);
+        configureProjectEditor(viewModel, new Project(-1), dao);
         return forwardView(req, viewModel, "project-form");
     }
 
     @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
-    public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+    public ResponseType commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
 
-        Project project = new Project(-1);
         try {
-            project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
+            final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
             project.setName(getParameter(req, String.class, "name").orElseThrow());
             getParameter(req, String.class, "description").ifPresent(project::setDescription);
             getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
@@ -187,30 +170,25 @@
 
             return ResponseType.HTML;
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            LOG.warn("Form validation failure: {}", ex.getMessage());
-            LOG.debug("Details:", ex);
-            final var viewModel = new ProjectEditView();
-            populate(viewModel, req, dao);
-            configure(viewModel, project, dao);
-            // TODO: error text
-            return forwardView(req, viewModel, "project-form");
+            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+            // TODO: implement - fix issue #21
+            return ResponseType.NONE;
         }
     }
 
-    @RequestMapping(requestPath = "view", method = HttpMethod.GET)
-    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
+    @RequestMapping(requestPath = "$project/versions/$version", method = HttpMethod.GET)
+    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObjects dao) throws SQLException, IOException {
         final var viewModel = new ProjectDetailsView();
-        populate(viewModel, req, dao);
+        populate(viewModel, pathParams, dao);
+        final var version = viewModel.getVersionFilter();
 
-        if (viewModel.getProjectInfo() == null) {
-            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
+        if (viewModel.getProjectInfo() == null || version == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
             return ResponseType.NONE;
         }
 
         final var issueDao = dao.getIssueDao();
 
-        final var version = viewModel.getVersionFilter();
-
         final var detailView = viewModel.getProjectDetails();
         final var issues = issueDao.list(version);
         for (var issue : issues) issueDao.joinVersionInformation(issue);
@@ -224,15 +202,14 @@
         return forwardView(req, viewModel, "project-details");
     }
 
-    @RequestMapping(requestPath = "versions", method = HttpMethod.GET)
-    public ResponseType versions(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    @RequestMapping(requestPath = "$project/versions/", method = HttpMethod.GET)
+    public ResponseType versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
         final var viewModel = new VersionsView();
-        populate(viewModel, req, dao);
-        viewModel.setVersionFilter(null);
+        populate(viewModel, pathParameters, dao);
 
         final var projectInfo = viewModel.getProjectInfo();
         if (projectInfo == null) {
-            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
             return ResponseType.NONE;
         }
 
@@ -244,49 +221,60 @@
         return forwardView(req, viewModel, "versions");
     }
 
-    @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
-    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    @RequestMapping(requestPath = "$project/versions/$version/edit", method = HttpMethod.GET)
+    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
         final var viewModel = new VersionEditView();
-        populate(viewModel, req, dao);
+        populate(viewModel, pathParameters, dao);
 
-        if (viewModel.getProjectInfo() == null) {
-            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
+        if (viewModel.getProjectInfo() == null || viewModel.getVersionFilter() == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
             return ResponseType.NONE;
         }
 
-        viewModel.setVersion(Optional.ofNullable(viewModel.getVersionFilter()).orElse(new Version(-1)));
+        viewModel.setVersion(viewModel.getVersionFilter());
 
         return forwardView(req, viewModel, "version-form");
     }
 
-    @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
-    public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
+    @RequestMapping(requestPath = "$project/create-version", method = HttpMethod.GET)
+    public ResponseType createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
+        final var viewModel = new VersionEditView();
+        populate(viewModel, pathParameters, dao);
 
-        var version = new Version(-1);
+        if (viewModel.getProjectInfo() == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return ResponseType.NONE;
+        }
+
+        viewModel.setVersion(viewModel.getVersionFilter());
+
+        return forwardView(req, viewModel, "version-form");
+    }
+
+    @RequestMapping(requestPath = "commit-version", method = HttpMethod.POST)
+    public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
+
         try {
             final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
-            version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
+            final var version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
             version.setName(getParameter(req, String.class, "name").orElseThrow());
             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/versions?pid=" + project.getId());
+            // TODO: improve building the redirect location
+            setRedirectLocation(req, "./projects/" + project.getId() + "/versions/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            LOG.warn("Form validation failure: {}", ex.getMessage());
-            LOG.debug("Details:", ex);
-            final var viewModel = new VersionEditView();
-            populate(viewModel, req, dao);
-            viewModel.setVersion(version);
-            // TODO: set Error Text
-            return forwardView(req, viewModel, "version-form");
+            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+            // TODO: implement - fix issue #21
+            return ResponseType.NONE;
         }
 
         return ResponseType.HTML;
     }
 
-    private void configure(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
+    private void configureProjectEditor(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
         issue.setProject(viewModel.getProjectInfo().getProject());
         viewModel.setIssue(issue);
         viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
@@ -296,30 +284,52 @@
         }
     }
 
-    @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
-    public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
+    @RequestMapping(requestPath = "$project/issues/$issue/edit", method = HttpMethod.GET)
+    public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
         final var viewModel = new IssueEditView();
-        populate(viewModel, req, dao);
+        populate(viewModel, pathParameters, dao);
 
-        final var issueParam = getParameter(req, Integer.class, "issue");
-        if (issueParam.isPresent()) {
-            final var issueDao = dao.getIssueDao();
-            final var issue = issueDao.find(issueParam.get());
-            issueDao.joinVersionInformation(issue);
-            req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, issue.getProject().getId());
-            configure(viewModel, issue, dao);
-        } else {
-            configure(viewModel, new Issue(-1), dao);
+        final var projectInfo = viewModel.getProjectInfo();
+        if (projectInfo == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return ResponseType.NONE;
         }
 
+        final var issueDao = dao.getIssueDao();
+        final var issue = issueDao.find(Functions.parseIntOrZero(pathParameters.get("issue")));
+        if (issue == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return ResponseType.NONE;
+        }
+
+        issueDao.joinVersionInformation(issue);
+        configureProjectEditor(viewModel, issue, dao);
+
         return forwardView(req, viewModel, "issue-form");
     }
 
-    @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
-    public ResponseType commitIssue(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
-        Issue issue = new Issue(-1);
+    @RequestMapping(requestPath = "$project/create-issue", method = HttpMethod.GET)
+    public ResponseType createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
+        final var viewModel = new IssueEditView();
+        populate(viewModel, pathParameters, dao);
+
+        final var projectInfo = viewModel.getProjectInfo();
+        if (projectInfo == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return ResponseType.NONE;
+        }
+
+        final var issue = new Issue(-1);
+        issue.setProject(projectInfo.getProject());
+        configureProjectEditor(viewModel, issue, dao);
+
+        return forwardView(req, viewModel, "issue-form");
+    }
+
+    @RequestMapping(requestPath = "commit-issue", method = HttpMethod.POST)
+    public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
         try {
-            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
+            final var issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
             issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
             getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
             getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
@@ -343,24 +353,19 @@
 
             dao.getIssueDao().saveOrUpdate(issue, issue.getProject());
 
-            // specifying the issue parameter keeps the edited issue as menu item
-            setRedirectLocation(req, "./projects/view?pid=" + issue.getProject().getId());
+            // TODO: fix issue #14
+            setRedirectLocation(req, "./projects/" + issue.getProject().getId() + "/versions/");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             return ResponseType.HTML;
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            // TODO: set request attribute with error text
-            LOG.warn("Form validation failure: {}", ex.getMessage());
-            LOG.debug("Details:", ex);
-            final var viewModel = new IssueEditView();
-            populate(viewModel, req, dao);
-            configure(viewModel, issue, dao);
-            // TODO: set Error Text
-            return forwardView(req, viewModel, "issue-form");
+            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+            // TODO: implement - fix issue #21
+            return ResponseType.NONE;
         }
     }
 
-    @RequestMapping(requestPath = "issues/comment", method = HttpMethod.POST)
+    @RequestMapping(requestPath = "commit-issue-comment", method = HttpMethod.POST)
     public ResponseType commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
         final var issueIdParam = getParameter(req, Integer.class, "issueid");
         if (issueIdParam.isEmpty()) {
@@ -383,20 +388,15 @@
 
             dao.getIssueDao().saveComment(issueComment);
 
-            // specifying the issue parameter keeps the edited issue as menu item
-            setRedirectLocation(req, "./projects/issues/edit?issue=" + issue.getId());
+            // TODO: fix redirect location (e.g. after fixing #24)
+            setRedirectLocation(req, "./projects/" + issue.getProject().getId()+"/issues/"+issue.getId()+"/edit");
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
 
             return ResponseType.HTML;
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            // TODO: set request attribute with error text
-            LOG.warn("Form validation failure: {}", ex.getMessage());
-            LOG.debug("Details:", ex);
-            final var viewModel = new IssueEditView();
-            populate(viewModel, req, dao);
-            configure(viewModel, issue, dao);
-            // TODO: set Error Text
-            return forwardView(req, viewModel, "issue-form");
+            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+            // TODO: implement - fix issue #21
+            return ResponseType.NONE;
         }
     }
 }
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -31,7 +31,8 @@
 <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueEditView" scope="request"/>
 <c:set var="issue" scope="page" value="${viewmodel.issue}" />
 
-<form action="./projects/issues/commit" method="post">
+<%-- TODO: change to ./issues/commit --%>
+<form action="./projects/commit-issue" method="post">
     <table class="formtable fullwidth">
         <colgroup>
             <col>
@@ -152,15 +153,8 @@
         <tr>
             <td colspan="2">
                 <input type="hidden" name="id" value="${issue.id}"/>
-                <c:choose>
-                    <c:when test="${not empty issue.project}">
-                        <c:set var="cancelUrl">./projects/view?pid=${issue.project.id}</c:set>
-                    </c:when>
-                    <c:otherwise>
-                        <c:set var="cancelUrl">./projects/</c:set>
-                    </c:otherwise>
-                </c:choose>
-                <a href="${cancelUrl}" class="button">
+                <%-- TODO: fix #14 --%>
+                <a href="./projects/${issue.project.id}/versions/" class="button">
                     <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
                 </a>
                 <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
@@ -172,7 +166,7 @@
 <hr class="comments-separator"/>
 <h2><fmt:message key="issue.comments"/></h2>
 <c:if test="${viewmodel.issue.id ge 0}">
-<form id="comment-form" action="./projects/issues/comment" method="post">
+<form id="comment-form" action="./projects/commit-issue-comment" method="post">
     <table class="formtable fullwidth">
         <tbody>
             <tr>
--- a/src/main/webapp/WEB-INF/jsp/issues.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/issues.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -28,6 +28,9 @@
 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
 
+<h1>TODO: REWRITE THIS PAGE</h1>
+<%--
+TODO: rewrite
 <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssuesView" scope="request"/>
 <c:set var="project" scope="page" value="${viewmodel.project}"/>
 <c:set var="version" scope="page" value="${viewmodel.version}"/>
@@ -50,4 +53,5 @@
 </div>
 
 <c:set var="issues" value="${viewmodel.issues}"/>
-<%@include file="../jspf/issue-list.jspf"%>
\ No newline at end of file
+<%@include file="../jspf/issue-list.jspf"%>
+--%>
\ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -35,10 +35,10 @@
 
 <div id="tool-area">
     <c:if test="${not empty viewmodel.versionFilter}">
-        <a href="./projects/versions/edit?vid=${viewmodel.versionFilter.id}" class="button"><fmt:message key="button.version.edit"/></a>
+        <a href="./projects/${project.id}/versions/${viewmodel.versionFilter.id}/edit" class="button"><fmt:message key="button.version.edit"/></a>
     </c:if>
-    <a href="./projects/versions/edit?vid=-1" class="button"><fmt:message key="button.version.create"/></a>
-    <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a>
+    <a href="./projects/${project.id}/create-version" class="button"><fmt:message key="button.version.create"/></a>
+    <a href="./projects/${project.id}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
 </div>
 
 <h2><fmt:message key="progress" /></h2>
--- a/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -33,20 +33,20 @@
 <c:forEach var="projectInfo" items="${viewmodel.projectList}">
     <c:set var="isActive" value="${viewmodel.projectInfo.project eq projectInfo.project}" />
     <div class="menuEntry level-0" <c:if test="${isActive}">data-active</c:if> >
-        <a href="projects/versions?pid=${projectInfo.project.id}">
+        <a href="projects/${projectInfo.project.id}/versions/">
             <c:out value="${projectInfo.project.name}"/>
         </a>
     </div>
     <c:if test="${isActive}">
         <!-- VERSIONS -->
         <div class="menuEntry level-1">
-            <a href="projects/versions?pid=${projectInfo.project.id}">
+            <a href="projects/${projectInfo.project.id}/versions/">
                 <fmt:message key="navmenu.versions"/>
             </a>
         </div>
         <div class="menuEntry level-2">
             <div class="navmenu-icon" style="background: black"></div>
-            <a href="projects/view?pid=${projectInfo.project.id}&vid=-1">
+            <a href="projects/${projectInfo.project.id}/versions/unassigned">
                 <fmt:message key="navmenu.unassigned" />
             </a>
         </div>
@@ -55,26 +55,27 @@
             <div class="menuEntry level-2" <c:if test="${isVersionActive}">data-active</c:if>
                     title="<fmt:message key="version.status.${version.status}" />">
                 <div class="navmenu-icon version-${version.status}"></div>
-                <a href="projects/view?pid=${projectInfo.project.id}&vid=${version.id}">
+                <a href="projects/${projectInfo.project.id}/versions/${version.id}">
                     <c:out value="${version.name}"/>
                 </a>
             </div>
         </c:forEach>
-        <!-- COMPONENTS -->
+        <%-- COMPONENTS
+        TODO: find a way to combine version and component into one URL
         <div class="menuEntry level-1">
-            <a href="projects/components?pid=${projectInfo.project.id}">
+            <a href="projects/${projectInfo.project.id}/components/">
                 <fmt:message key="navmenu.components"/>
             </a>
         </div>
         <div class="menuEntry level-2">
             <div class="navmenu-icon" style="background: black"></div>
-            <a href="projects/view?pid=${projectInfo.project.id}&cid=0">
+            <a href="projects/${projectInfo.project.id}/components/">
                 <fmt:message key="navmenu.all" />
             </a>
         </div>
         <div class="menuEntry level-2">
             <div class="navmenu-icon" style="background: black"></div>
-            <a href="projects/view?pid=${projectInfo.project.id}&cid=-1">
+            <a href="projects/${projectInfo.project.id}/components/unassigned">
                 <fmt:message key="navmenu.unassigned" />
             </a>
         </div>
@@ -87,5 +88,6 @@
                 </a>
             </div>
         </c:forEach>
+        --%>
     </c:if>
 </c:forEach>
--- a/src/main/webapp/WEB-INF/jsp/projects.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/projects.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -37,7 +37,7 @@
 </c:if>
 
 <div id="tool-area">
-    <a href="./projects/edit?pid=-1" class="button"><fmt:message key="button.create"/></a>
+    <a href="./projects/create" class="button"><fmt:message key="button.create"/></a>
 </div>
 
 <c:if test="${not empty viewmodel.projectList}">
@@ -68,8 +68,8 @@
         <c:forEach var="projectInfo" items="${viewmodel.projectList}">
             <c:set var="project" scope="page" value="${projectInfo.project}"/>
             <tr class="nowrap">
-                <td style="width: 2em;"><a href="./projects/edit?pid=${project.id}">&#x270e;</a></td>
-                <td><a href="./projects/versions?pid=${project.id}"><c:out value="${project.name}"/></a>
+                <td style="width: 2em;"><a href="./projects/${project.id}/edit">&#x270e;</a></td>
+                <td><a href="./projects/${project.id}/versions/"><c:out value="${project.name}"/></a>
                 </td>
                 <td>
                     <c:if test="${not empty project.repoUrl}">
@@ -79,12 +79,12 @@
                 </td>
                 <td class="hright">
                     <c:if test="${not empty projectInfo.latestVersion}">
-                        <a href="./projects/view?pid=${project.id}&vid=${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a>
+                        <a href="./projects/${project.id}/versions/${projectInfo.latestVersion.id}/"><c:out value="${projectInfo.latestVersion.name}"/></a>
                     </c:if>
                 </td>
                 <td class="hright">
                     <c:if test="${not empty projectInfo.nextVersion}">
-                        <a href="./projects/view?pid=${project.id}&vid=${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a>
+                        <a href="./projects/${project.id}/versions/${projectInfo.nextVersion.id}/"><c:out value="${projectInfo.nextVersion.name}"/></a>
                     </c:if>
                 </td>
                 <td class="hright">${projectInfo.issueSummary.open}</td>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -32,7 +32,7 @@
 <c:set var="version" scope="page" value="${viewmodel.version}"/>
 <c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
 
-<form action="./projects/versions/commit" method="post">
+<form action="./projects/commit-version" method="post">
     <table class="formtable" style="width: 35ch">
         <colgroup>
             <col>
@@ -73,7 +73,7 @@
         <tr>
             <td colspan="2">
                 <input type="hidden" name="id" value="${version.id}"/>
-                <a href="./projects/versions?pid=${project.id}" class="button">
+                <a href="./projects/${project.id}/versions/" class="button">
                     <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
                 </a>
                 <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
--- a/src/main/webapp/WEB-INF/jsp/versions.jsp	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/versions.jsp	Thu Oct 15 20:02:30 2020 +0200
@@ -34,8 +34,8 @@
 <%@include file="../jspf/project-header.jspf"%>
 
 <div id="tool-area">
-    <a href="./projects/versions/edit?vid=-1" class="button"><fmt:message key="button.version.create"/></a>
-    <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a>
+    <a href="./projects/${project.id}/create-version" class="button"><fmt:message key="button.version.create"/></a>
+    <a href="./projects/${project.id}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
 </div>
 
 <h2><fmt:message key="progress" /></h2>
@@ -78,9 +78,9 @@
     <tbody>
         <c:forEach var="versionInfo" items="${viewmodel.versionInfo}" >
         <tr>
-            <td rowspan="2" style="width: 2em;"><a href="./projects/versions/edit?vid=${versionInfo.version.id}">&#x270e;</a></td>
+            <td rowspan="2" style="width: 2em;"><a href="./projects/${project.id}/versions/${versionInfo.version.id}/edit">&#x270e;</a></td>
             <td rowspan="2">
-                <a href="projects/view?pid=${viewmodel.projectInfo.project.id}&vid=${versionInfo.version.id}">
+                <a href="./projects/${project.id}/versions/${versionInfo.version.id}">
                     <c:out value="${versionInfo.version.name}"/>
                 </a>
                 <div class="version-tag version-${versionInfo.version.status}">
--- a/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Thu Oct 15 18:36:05 2020 +0200
+++ b/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Thu Oct 15 20:02:30 2020 +0200
@@ -18,7 +18,7 @@
         <tr>
             <td>
                 <span class="phase-${issue.status.phase}">
-                    <a href="./projects/issues/edit?issue=${issue.id}">
+                    <a href="./projects/${issue.project.id}/issues/${issue.id}/edit">
                         #${issue.id}&nbsp;-&nbsp;<c:out value="${issue.subject}" />
                     </a>
                 </span>

mercurial