Mon, 04 Jan 2021 17:30:10 +0100
automatically select version/component when creating new issues under active filters
/* * 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.modules; import de.uapcore.lightpit.*; import de.uapcore.lightpit.dao.DataAccessObject; import de.uapcore.lightpit.entities.*; import de.uapcore.lightpit.filter.AllFilter; import de.uapcore.lightpit.filter.IssueFilter; import de.uapcore.lightpit.filter.NoneFilter; import de.uapcore.lightpit.filter.SpecificFilter; import de.uapcore.lightpit.types.IssueCategory; import de.uapcore.lightpit.types.IssueStatus; import de.uapcore.lightpit.types.VersionStatus; import de.uapcore.lightpit.types.WebColor; import de.uapcore.lightpit.viewmodel.*; import de.uapcore.lightpit.viewmodel.util.IssueSorter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.Date; import java.sql.SQLException; import java.util.NoSuchElementException; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @WebServlet( name = "ProjectsModule", urlPatterns = "/projects/*" ) public final class ProjectsModule extends AbstractLightPITServlet { private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class); @Override protected String getResourceBundleName() { return "localization.projects"; } private static int parseIntOrZero(String str) { try { return Integer.parseInt(str); } catch (NumberFormatException ex) { return 0; } } private void populate(ProjectView viewModel, PathParameters pathParameters, DataAccessObject dao) { dao.listProjects().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add); if (pathParameters == null) return; // Select Project final var project = dao.findProjectByNode(pathParameters.get("project")); if (project == null) return; final var info = new ProjectInfo(project); info.setVersions(dao.listVersions(project)); info.setComponents(dao.listComponents(project)); info.setIssueSummary(dao.collectIssueSummary(project)); viewModel.setProjectInfo(info); // Select Version final var versionNode = pathParameters.get("version"); if (versionNode != null) { if ("no-version".equals(versionNode)) { viewModel.setVersionFilter(ProjectView.NO_VERSION); } else if ("all-versions".equals(versionNode)) { viewModel.setVersionFilter(ProjectView.ALL_VERSIONS); } else { viewModel.setVersionFilter(dao.findVersionByNode(project, versionNode)); } } // Select Component final var componentNode = pathParameters.get("component"); if (componentNode != null) { if ("no-component".equals(componentNode)) { viewModel.setComponentFilter(ProjectView.NO_COMPONENT); } else if ("all-components".equals(componentNode)) { viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS); } else { viewModel.setComponentFilter(dao.findComponentByNode(project, componentNode)); } } } private static String sanitizeNode(String node, String defaultValue) { String result = node == null || node.isBlank() ? defaultValue : node; result = result.replace('/', '-'); if (result.equals(".") || result.equals("..")) { return "_"+result; } else { return result; } } private void forwardView(HttpServletRequest req, HttpServletResponse resp, ProjectView viewModel, String name) throws ServletException, IOException { setViewModel(req, viewModel); setContentPage(req, name); setStylesheet(req, "projects"); setNavigationMenu(req, "project-navmenu"); renderSite(req, resp); } @RequestMapping(method = HttpMethod.GET) public void index(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws ServletException, IOException { final var viewModel = new ProjectView(); populate(viewModel, null, dao); for (var info : viewModel.getProjectList()) { info.setVersions(dao.listVersions(info.getProject())); info.setIssueSummary(dao.collectIssueSummary(info.getProject())); } forwardView(req, resp, viewModel, "projects"); } private void configureProjectEditor(ProjectEditView viewModel, Project project, DataAccessObject dao) { viewModel.setProject(project); viewModel.setUsers(dao.listUsers()); } @RequestMapping(requestPath = "$project/edit", method = HttpMethod.GET) public void edit(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws IOException, SQLException, ServletException { final var viewModel = new ProjectEditView(); populate(viewModel, pathParams, dao); if (!viewModel.isProjectInfoPresent()) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } configureProjectEditor(viewModel, viewModel.getProjectInfo().getProject(), dao); forwardView(req, resp, viewModel, "project-form"); } @RequestMapping(requestPath = "create", method = HttpMethod.GET) public void create(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws SQLException, ServletException, IOException { final var viewModel = new ProjectEditView(); populate(viewModel, null, dao); configureProjectEditor(viewModel, new Project(-1), dao); forwardView(req, resp, viewModel, "project-form"); } @RequestMapping(requestPath = "commit", method = HttpMethod.POST) public void commit(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { try { final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow()); project.setName(getParameter(req, String.class, "name").orElseThrow()); final var node = getParameter(req, String.class, "node").orElse(null); project.setNode(sanitizeNode(node, project.getName())); getParameter(req, Integer.class, "ordinal").ifPresent(project::setOrdinal); getParameter(req, String.class, "description").ifPresent(project::setDescription); getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl); getParameter(req, Integer.class, "owner").map( ownerId -> ownerId >= 0 ? new User(ownerId) : null ).ifPresent(project::setOwner); if (project.getId() > 0) { dao.updateProject(project); } else { dao.insertProject(project); } setRedirectLocation(req, "./projects/"); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); LOG.debug("Successfully updated project {}", project.getName()); renderSite(req, resp); } catch (NoSuchElementException | IllegalArgumentException ex) { resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); // TODO: implement - fix issue #21 } } @RequestMapping(requestPath = "$project/$component/$version/issues/", method = HttpMethod.GET) public void issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObject dao) throws SQLException, IOException, ServletException { final var viewModel = new ProjectDetailsView(); populate(viewModel, pathParams, dao); if (!viewModel.isEveryFilterValid()) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var project = viewModel.getProjectInfo().getProject(); final var version = viewModel.getVersionFilter(); final var component = viewModel.getComponentFilter(); // TODO: use new IssueFilter class for the ViewModel final var projectFilter = new SpecificFilter<>(project); final IssueFilter filter; if (version.equals(ProjectView.NO_VERSION)) { if (component.equals(ProjectView.ALL_COMPONENTS)) { filter = new IssueFilter(projectFilter, new NoneFilter<>(), new AllFilter<>() ); } else if (component.equals(ProjectView.NO_COMPONENT)) { filter = new IssueFilter(projectFilter, new NoneFilter<>(), new NoneFilter<>() ); } else { filter = new IssueFilter(projectFilter, new NoneFilter<>(), new SpecificFilter<>(component) ); } } else if (version.equals(ProjectView.ALL_VERSIONS)) { if (component.equals(ProjectView.ALL_COMPONENTS)) { filter = new IssueFilter(projectFilter, new AllFilter<>(), new AllFilter<>() ); } else if (component.equals(ProjectView.NO_COMPONENT)) { filter = new IssueFilter(projectFilter, new AllFilter<>(), new NoneFilter<>() ); } else { filter = new IssueFilter(projectFilter, new AllFilter<>(), new SpecificFilter<>(component) ); } } else { if (component.equals(ProjectView.ALL_COMPONENTS)) { filter = new IssueFilter(projectFilter, new SpecificFilter<>(version), new AllFilter<>() ); } else if (component.equals(ProjectView.NO_COMPONENT)) { filter = new IssueFilter(projectFilter, new SpecificFilter<>(version), new NoneFilter<>() ); } else { filter = new IssueFilter(projectFilter, new SpecificFilter<>(version), new SpecificFilter<>(component) ); } } final var issues = dao.listIssues(filter); issues.sort(new IssueSorter( new IssueSorter.Criteria(IssueSorter.Field.DONE, true), new IssueSorter.Criteria(IssueSorter.Field.ETA, true), new IssueSorter.Criteria(IssueSorter.Field.UPDATED, false) )); viewModel.getProjectDetails().updateDetails(issues); if (version.getId() > 0) viewModel.getProjectDetails().updateVersionInfo(version); forwardView(req, resp, viewModel, "project-details"); } @RequestMapping(requestPath = "$project/versions/", method = HttpMethod.GET) public void versions(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException { final var viewModel = new VersionsView(); populate(viewModel, pathParameters, dao); final var projectInfo = viewModel.getProjectInfo(); if (projectInfo == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var issues = dao.listIssues( new IssueFilter( new SpecificFilter<>(projectInfo.getProject()), new AllFilter<>(), new AllFilter<>() ) ); viewModel.update(projectInfo.getVersions(), issues); forwardView(req, resp, viewModel, "versions"); } @RequestMapping(requestPath = "$project/versions/$version/edit", method = HttpMethod.GET) public void editVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { final var viewModel = new VersionEditView(); populate(viewModel, pathParameters, dao); if (viewModel.getProjectInfo() == null || viewModel.getVersionFilter() == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } viewModel.setVersion(viewModel.getVersionFilter()); forwardView(req, resp, viewModel, "version-form"); } @RequestMapping(requestPath = "$project/create-version", method = HttpMethod.GET) public void createVersion(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { final var viewModel = new VersionEditView(); populate(viewModel, pathParameters, dao); if (viewModel.getProjectInfo() == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } viewModel.setVersion(new Version(-1, viewModel.getProjectInfo().getProject().getId())); forwardView(req, resp, viewModel, "version-form"); } @RequestMapping(requestPath = "commit-version", method = HttpMethod.POST) public void commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { try { final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow()); if (project == null) { // TODO: improve error handling, because not found is not correct for this POST request resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), project.getId()); version.setName(getParameter(req, String.class, "name").orElseThrow()); final var node = getParameter(req, String.class, "node").orElse(null); version.setNode(sanitizeNode(node, version.getName())); getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal); version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow())); if (version.getId() > 0) { dao.updateVersion(version); } else { dao.insertVersion(version); } setRedirectLocation(req, "./projects/" + project.getNode() + "/versions/"); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); renderSite(req, resp); } catch (NoSuchElementException | IllegalArgumentException ex) { resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); // TODO: implement - fix issue #21 } } @RequestMapping(requestPath = "$project/components/", method = HttpMethod.GET) public void components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException { final var viewModel = new ComponentsView(); populate(viewModel, pathParameters, dao); final var projectInfo = viewModel.getProjectInfo(); if (projectInfo == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var issues = dao.listIssues( new IssueFilter( new SpecificFilter<>(projectInfo.getProject()), new AllFilter<>(), new AllFilter<>() ) ); viewModel.update(projectInfo.getComponents(), issues); forwardView(req, resp, viewModel, "components"); } @RequestMapping(requestPath = "$project/components/$component/edit", method = HttpMethod.GET) public void editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { final var viewModel = new ComponentEditView(); populate(viewModel, pathParameters, dao); if (viewModel.getProjectInfo() == null || viewModel.getComponentFilter() == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } viewModel.setComponent(viewModel.getComponentFilter()); viewModel.setUsers(dao.listUsers()); forwardView(req, resp, viewModel, "component-form"); } @RequestMapping(requestPath = "$project/create-component", method = HttpMethod.GET) public void createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { final var viewModel = new ComponentEditView(); populate(viewModel, pathParameters, dao); if (viewModel.getProjectInfo() == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } viewModel.setComponent(new Component(-1, viewModel.getProjectInfo().getProject().getId())); viewModel.setUsers(dao.listUsers()); forwardView(req, resp, viewModel, "component-form"); } @RequestMapping(requestPath = "commit-component", method = HttpMethod.POST) public void commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { try { final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow()); if (project == null) { // TODO: improve error handling, because not found is not correct for this POST request resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var component = new Component(getParameter(req, Integer.class, "id").orElseThrow(), project.getId()); component.setName(getParameter(req, String.class, "name").orElseThrow()); final var node = getParameter(req, String.class, "node").orElse(null); component.setNode(sanitizeNode(node, component.getName())); component.setColor(getParameter(req, WebColor.class, "color").orElseThrow()); getParameter(req, Integer.class, "ordinal").ifPresent(component::setOrdinal); getParameter(req, Integer.class, "lead").map( userid -> userid >= 0 ? new User(userid) : null ).ifPresent(component::setLead); getParameter(req, String.class, "description").ifPresent(component::setDescription); if (component.getId() > 0) { dao.updateComponent(component); } else { dao.insertComponent(component); } setRedirectLocation(req, "./projects/" + project.getNode() + "/components/"); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); renderSite(req, resp); } catch (NoSuchElementException | IllegalArgumentException ex) { resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); // TODO: implement - fix issue #21 } } private void configureIssueEditor(IssueEditView viewModel, Issue issue, DataAccessObject dao) { final var project = viewModel.getProjectInfo().getProject(); issue.setProject(project); // automatically set current project for new issues viewModel.setIssue(issue); viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions()); viewModel.setUsers(dao.listUsers()); viewModel.setComponents(dao.listComponents(project)); } @RequestMapping(requestPath = "$project/issues/$issue/view", method = HttpMethod.GET) public void viewIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { final var viewModel = new IssueDetailView(); populate(viewModel, pathParameters, dao); final var projectInfo = viewModel.getProjectInfo(); if (projectInfo == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue"))); if (issue == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } viewModel.setIssue(issue); viewModel.setComments(dao.listComments(issue)); viewModel.processMarkdown(); forwardView(req, resp, viewModel, "issue-view"); } // TODO: why should the issue editor be child of $project? @RequestMapping(requestPath = "$project/issues/$issue/edit", method = HttpMethod.GET) public void editIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, SQLException, ServletException { final var viewModel = new IssueEditView(); populate(viewModel, pathParameters, dao); final var projectInfo = viewModel.getProjectInfo(); if (projectInfo == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var issue = dao.findIssue(parseIntOrZero(pathParameters.get("issue"))); if (issue == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } configureIssueEditor(viewModel, issue, dao); forwardView(req, resp, viewModel, "issue-form"); } @RequestMapping(requestPath = "$project/create-issue", method = HttpMethod.GET) public void createIssue(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObject dao) throws IOException, ServletException { final var viewModel = new IssueEditView(); populate(viewModel, pathParameters, dao); final var projectInfo = viewModel.getProjectInfo(); if (projectInfo == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } setAttributeFromParameter(req, "more"); setAttributeFromParameter(req, "cid"); setAttributeFromParameter(req, "vid"); final var issue = new Issue(-1, projectInfo.getProject(), null); issue.setProject(projectInfo.getProject()); configureIssueEditor(viewModel, issue, dao); forwardView(req, resp, viewModel, "issue-form"); } @RequestMapping(requestPath = "commit-issue", method = HttpMethod.POST) public void commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { try { final var project = dao.findProject(getParameter(req, Integer.class, "pid").orElseThrow()); if (project == null) { // TODO: improve error handling, because not found is not correct for this POST request resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } final var componentId = getParameter(req, Integer.class, "component"); final Component component; if (componentId.isPresent()) { component = dao.findComponent(componentId.get()); } else { component = null; } final var issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), project, component); getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory); getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus); issue.setSubject(getParameter(req, String.class, "subject").orElseThrow()); getParameter(req, Integer.class, "assignee").map(userid -> { if (userid >= 0) { return new User(userid); } else if (userid == -2) { return Optional.ofNullable(component).map(Component::getLead).orElse(null); } else { return null; } } ).ifPresent(issue::setAssignee); getParameter(req, String.class, "description").ifPresent(issue::setDescription); getParameter(req, Date.class, "eta").ifPresent(issue::setEta); getParameter(req, Integer[].class, "affected") .map(Stream::of) .map(stream -> stream.map(id -> new Version(id, project.getId())) .collect(Collectors.toList()) ).ifPresent(issue::setAffectedVersions); getParameter(req, Integer[].class, "resolved") .map(Stream::of) .map(stream -> stream.map(id -> new Version(id, project.getId())) .collect(Collectors.toList()) ).ifPresent(issue::setResolvedVersions); if (issue.getId() > 0) { dao.updateIssue(issue); } else { dao.insertIssue(issue); } if (getParameter(req, Boolean.class, "create-another").orElse(false)) { // TODO: fix #38 - automatically select component (and version) setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/create-issue?more=true"); } else{ setRedirectLocation(req, "./projects/" + issue.getProject().getNode() + "/issues/" + issue.getId() + "/view"); } setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); renderSite(req, resp); } catch (NoSuchElementException | IllegalArgumentException ex) { resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); // TODO: implement - fix issue #21 } } @RequestMapping(requestPath = "commit-issue-comment", method = HttpMethod.POST) public void commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObject dao) throws IOException, ServletException { final var issueIdParam = getParameter(req, Integer.class, "issueid"); if (issueIdParam.isEmpty()) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form."); return; } final var issue = dao.findIssue(issueIdParam.get()); if (issue == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } try { final var issueComment = new IssueComment(getParameter(req, Integer.class, "commentid").orElse(-1), issue.getId()); issueComment.setComment(getParameter(req, String.class, "comment").orElse("")); if (issueComment.getComment().isBlank()) { throw new IllegalArgumentException("comment.null"); } LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId()); if (req.getRemoteUser() != null) { Optional.ofNullable(dao.findUserByName(req.getRemoteUser())).ifPresent(issueComment::setAuthor); } dao.insertComment(issueComment); setRedirectLocation(req, "./projects/" + issue.getProject().getNode()+"/issues/"+issue.getId()+"/view"); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); renderSite(req, resp); } catch (NoSuchElementException | IllegalArgumentException ex) { resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); // TODO: implement - fix issue #21 } } }