Sun, 21 Jun 2020 12:31:38 +0200
adds graphical visualization for issue type and status
/* * 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.DataAccessObjects; import de.uapcore.lightpit.entities.*; import de.uapcore.lightpit.viewmodel.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Date; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; import static de.uapcore.lightpit.Functions.fqn; @WebServlet( name = "ProjectsModule", urlPatterns = "/projects/*" ) public final class ProjectsModule extends AbstractLightPITServlet { private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class); public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project"); public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue"); public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version"); private class SessionSelection { final HttpSession session; final HttpServletRequest req; final DataAccessObjects dao; Project project; Version version; Issue issue; SessionSelection(HttpServletRequest req, DataAccessObjects dao) { this.req = req; this.dao = dao; session = req.getSession(); } void newProject() { project = null; version = null; issue = null; updateAttributes(); project = new Project(-1); updateAttributes(); } void newVersion() throws SQLException { project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); syncProject(); version = null; issue = null; updateAttributes(); version = new Version(-1); version.setProject(project); updateAttributes(); } void newIssue() throws SQLException { project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); syncProject(); version = null; issue = null; updateAttributes(); issue = new Issue(-1); issue.setProject(project); updateAttributes(); } void selectVersion(Version selectedVersion) throws SQLException { issue = null; version = selectedVersion; if (!version.getProject().equals(project)) { project = dao.getProjectDao().find(version.getProject().getId()); } // our object contains more details version.setProject(project); updateAttributes(); } void selectIssue(Issue selectedIssue) throws SQLException { issue = selectedIssue; if (!issue.getProject().equals(project)) { project = dao.getProjectDao().find(issue.getProject().getId()); } // our object contains more details issue.setProject(project); if (!issue.getResolvedVersions().contains(version) && !issue.getAffectedVersions().contains(version)) { version = null; } updateAttributes(); } void syncProject() throws SQLException { final var projectSelection = getParameter(req, Integer.class, "pid"); if (projectSelection.isPresent()) { final var selectedProject = dao.getProjectDao().find(projectSelection.get()); if (!Objects.equals(selectedProject, project)) { // reset version and issue if project changed version = null; issue = null; } project = selectedProject; } else { project = project == null ? null : dao.getProjectDao().find(project.getId()); } } void syncVersion() throws SQLException { final var versionSelection = getParameter(req, Integer.class, "vid"); if (versionSelection.isPresent()) { if (versionSelection.get() < 0) { version = null; } else { final var selectedVersion = dao.getVersionDao().find(versionSelection.get()); if (!Objects.equals(selectedVersion, version)) { issue = null; } selectVersion(selectedVersion); } } else { version = version == null ? null : dao.getVersionDao().find(version.getId()); } } void syncIssue() throws SQLException { final var issueSelection = getParameter(req, Integer.class, "issue"); if (issueSelection.isPresent()) { final var selectedIssue = dao.getIssueDao().find(issueSelection.get()); dao.getIssueDao().joinVersionInformation(selectedIssue); selectIssue(selectedIssue); } else { issue = issue == null ? null : dao.getIssueDao().find(issue.getId()); } } void sync() throws SQLException { project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); version = (Version) session.getAttribute(SESSION_ATTR_SELECTED_VERSION); issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE); syncProject(); syncVersion(); syncIssue(); updateAttributes(); } private void updateAttributes() { session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project); session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version); session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue); } } @Override protected String getResourceBundleName() { return "localization.projects"; } private static final int BREADCRUMB_LEVEL_ROOT = 0; private static final int BREADCRUMB_LEVEL_PROJECT = 1; private static final int BREADCRUMB_LEVEL_VERSION = 2; private static final int BREADCRUMB_LEVEL_ISSUE_LIST = 3; private static final int BREADCRUMB_LEVEL_ISSUE = 4; /** * Creates the breadcrumb menu. * * @param level the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue) * @param selection the currently selected objects * @return a dynamic breadcrumb menu trying to display as many levels as possible */ private List<MenuEntry> getBreadcrumbs(int level, SessionSelection selection) { MenuEntry entry; final var breadcrumbs = new ArrayList<MenuEntry>(); entry = new MenuEntry(new ResourceKey("localization.lightpit", "menu.projects"), "projects/"); breadcrumbs.add(entry); if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true); if (selection.project != null) { if (selection.project.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"), "projects/edit"); } else { entry = new MenuEntry(selection.project.getName(), "projects/view?pid=" + selection.project.getId()); } if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true); breadcrumbs.add(entry); } if (selection.version != null) { if (selection.version.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"), "projects/versions/edit"); } else { entry = new MenuEntry(selection.version.getName(), "projects/versions/view?vid=" + selection.version.getId()); } if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true); breadcrumbs.add(entry); } if (selection.project != null) { String path = "projects/issues/?pid=" + selection.project.getId(); if (selection.version != null) { path += "&vid="+selection.version.getId(); } entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"), path); if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true); breadcrumbs.add(entry); } if (selection.issue != null) { if (selection.issue.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"), "projects/issues/edit"); } else { entry = new MenuEntry("#" + selection.issue.getId(), // TODO: maybe change link to a view rather than directly opening the editor "projects/issues/edit?issue=" + selection.issue.getId()); } if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true); breadcrumbs.add(entry); } return breadcrumbs; } @RequestMapping(method = HttpMethod.GET) public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException { final var sessionSelection = new SessionSelection(req, dao); sessionSelection.sync(); final var projectDao = dao.getProjectDao(); final var versionDao = dao.getVersionDao(); final var projectList = projectDao.list(); final var viewModel = new ProjectIndexView(); for (var project : projectList) { final var info = new ProjectInfo(project); info.setVersions(versionDao.list(project)); info.setIssueSummary(projectDao.getIssueSummary(project)); viewModel.getProjects().add(info); } setViewModel(req, viewModel); setContentPage(req, "projects"); setStylesheet(req, "projects"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ROOT, sessionSelection)); return ResponseType.HTML; } private ProjectEditView configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { final var viewModel = new ProjectEditView(); viewModel.setProject(selection.project); viewModel.setUsers(dao.getUserDao().list()); setViewModel(req, viewModel); setContentPage(req, "project-form"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection)); return viewModel; } @RequestMapping(requestPath = "edit", method = HttpMethod.GET) public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { final var selection = new SessionSelection(req, dao); if (getParameter(req, Integer.class, "pid").isEmpty()) { selection.newProject(); } else { selection.sync(); } configureEditForm(req, dao, selection); return ResponseType.HTML; } @RequestMapping(requestPath = "commit", method = HttpMethod.POST) public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { Project project = new Project(-1); try { project = new Project(getParameter(req, Integer.class, "id").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); getParameter(req, Integer.class, "owner").map( ownerId -> ownerId >= 0 ? new User(ownerId) : null ).ifPresent(project::setOwner); dao.getProjectDao().saveOrUpdate(project); setRedirectLocation(req, "./projects/"); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); LOG.debug("Successfully updated project {}", project.getName()); } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); final var selection = new SessionSelection(req, dao); selection.project = project; final var vm = configureEditForm(req, dao, selection); vm.setErrorText(ex.getMessage()); // TODO: error text } return ResponseType.HTML; } @RequestMapping(requestPath = "view", method = HttpMethod.GET) public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { final var selection = new SessionSelection(req, dao); selection.sync(); if (selection.project == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected."); return ResponseType.NONE; } final var versionDao = dao.getVersionDao(); final var issueDao = dao.getIssueDao(); final var viewModel = new ProjectView(selection.project); final var issues = issueDao.list(selection.project); for (var issue : issues) issueDao.joinVersionInformation(issue); viewModel.setIssues(issues); viewModel.setVersions(versionDao.list(selection.project)); viewModel.updateVersionInfo(); setViewModel(req, viewModel); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection)); setContentPage(req, "project-details"); setStylesheet(req, "projects"); return ResponseType.HTML; } @RequestMapping(requestPath = "versions/view", method = HttpMethod.GET) public ResponseType viewVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { final var selection = new SessionSelection(req, dao); selection.sync(); if (selection.version == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return ResponseType.NONE; } final var issueDao = dao.getIssueDao(); final var viewModel = new VersionView(selection.version); final var issues = issueDao.list(selection.version); for (var issue : issues) issueDao.joinVersionInformation(issue); viewModel.setIssues(issues); setViewModel(req, viewModel); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection)); setContentPage(req, "version"); setStylesheet(req, "projects"); return ResponseType.HTML; } private VersionEditView configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { final var viewModel = new VersionEditView(selection.version); if (selection.version.getProject() == null) { viewModel.setProjects(dao.getProjectDao().list()); } setViewModel(req, viewModel); setContentPage(req, "version-form"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection)); return viewModel; } @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET) public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException { final var selection = new SessionSelection(req, dao); if (getParameter(req, Integer.class, "vid").isEmpty()) { selection.newVersion(); } else { selection.sync(); } configureEditVersionForm(req, dao, selection); return ResponseType.HTML; } @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST) public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { var version = new Version(-1); try { version = new Version(getParameter(req, Integer.class, "id").orElseThrow()); version.setProject(new Project(getParameter(req, Integer.class, "pid").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); // specifying the pid parameter will purposely reset the session selected version! setRedirectLocation(req, "./projects/view?pid="+version.getProject().getId()); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); final var selection = new SessionSelection(req, dao); selection.selectVersion(version); final var viewModel = configureEditVersionForm(req, dao, selection); // TODO: set Error Text } return ResponseType.HTML; } private IssueEditView configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { final var viewModel = new IssueEditView(selection.issue); if (selection.issue.getProject() == null) { viewModel.setProjects(dao.getProjectDao().list()); } else { viewModel.setVersions(dao.getVersionDao().list(selection.issue.getProject())); } viewModel.setUsers(dao.getUserDao().list()); setViewModel(req, viewModel); setContentPage(req, "issue-form"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection)); return viewModel; } @RequestMapping(requestPath = "issues/", method = HttpMethod.GET) public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { final var selection = new SessionSelection(req, dao); selection.sync(); if (selection.project == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected."); return ResponseType.NONE; } final var viewModel = new IssuesView(); viewModel.setProject(selection.project); if (selection.version == null) { viewModel.setIssues(dao.getIssueDao().list(selection.project)); } else { viewModel.setVersion(selection.version); viewModel.setIssues(dao.getIssueDao().list(selection.version)); } setViewModel(req, viewModel); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, selection)); setContentPage(req, "issues"); setStylesheet(req, "projects"); return ResponseType.HTML; } @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET) public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { final var selection = new SessionSelection(req, dao); if (getParameter(req, Integer.class, "issue").isEmpty()) { selection.newIssue(); } else { selection.sync(); } configureEditIssueForm(req, dao, selection); return ResponseType.HTML; } @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST) public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { Issue issue = new Issue(-1); try { 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); issue.setSubject(getParameter(req, String.class, "subject").orElseThrow()); getParameter(req, Integer.class, "assignee").map( userid -> userid >= 0 ? new User(userid) : 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(Version::new).collect(Collectors.toList()) ).ifPresent(issue::setAffectedVersions); getParameter(req, Integer[].class, "resolved") .map(Stream::of) .map(stream -> stream.map(Version::new).collect(Collectors.toList()) ).ifPresent(issue::setResolvedVersions); dao.getIssueDao().saveOrUpdate(issue); // specifying the issue parameter keeps the edited issue as breadcrumb setRedirectLocation(req, "./projects/issues/?issue="+issue.getId()); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); } 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 selection = new SessionSelection(req, dao); selection.selectIssue(issue); final var viewModel = configureEditIssueForm(req, dao, selection); // TODO: set Error Text } return ResponseType.HTML; } }