Sun, 21 Jun 2020 11:38:16 +0200
simplifies version overviews by removing "scheduled issues"
1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3 *
4 * Copyright 2018 Mike Becker. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 * POSSIBILITY OF SUCH DAMAGE.
27 *
28 */
29 package de.uapcore.lightpit.modules;
32 import de.uapcore.lightpit.*;
33 import de.uapcore.lightpit.dao.DataAccessObjects;
34 import de.uapcore.lightpit.entities.*;
35 import de.uapcore.lightpit.viewmodel.*;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import javax.servlet.annotation.WebServlet;
40 import javax.servlet.http.HttpServletRequest;
41 import javax.servlet.http.HttpServletResponse;
42 import javax.servlet.http.HttpSession;
43 import java.io.IOException;
44 import java.sql.Date;
45 import java.sql.SQLException;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.NoSuchElementException;
49 import java.util.Objects;
50 import java.util.stream.Collectors;
51 import java.util.stream.Stream;
53 import static de.uapcore.lightpit.Functions.fqn;
55 @WebServlet(
56 name = "ProjectsModule",
57 urlPatterns = "/projects/*"
58 )
59 public final class ProjectsModule extends AbstractLightPITServlet {
61 private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
63 public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
64 public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue");
65 public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
67 private class SessionSelection {
68 final HttpSession session;
69 final HttpServletRequest req;
70 final DataAccessObjects dao;
71 Project project;
72 Version version;
73 Issue issue;
75 SessionSelection(HttpServletRequest req, DataAccessObjects dao) {
76 this.req = req;
77 this.dao = dao;
78 session = req.getSession();
79 }
81 void newProject() {
82 project = null;
83 version = null;
84 issue = null;
85 updateAttributes();
86 project = new Project(-1);
87 updateAttributes();
88 }
90 void newVersion() throws SQLException {
91 project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
92 syncProject();
93 version = null;
94 issue = null;
95 updateAttributes();
96 version = new Version(-1);
97 version.setProject(project);
98 updateAttributes();
99 }
101 void newIssue() throws SQLException {
102 project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
103 syncProject();
104 version = null;
105 issue = null;
106 updateAttributes();
107 issue = new Issue(-1);
108 issue.setProject(project);
109 updateAttributes();
110 }
112 void selectVersion(Version selectedVersion) throws SQLException {
113 issue = null;
114 version = selectedVersion;
115 if (!version.getProject().equals(project)) {
116 project = dao.getProjectDao().find(version.getProject().getId());
117 }
118 // our object contains more details
119 version.setProject(project);
120 updateAttributes();
121 }
123 void selectIssue(Issue selectedIssue) throws SQLException {
124 issue = selectedIssue;
125 if (!issue.getProject().equals(project)) {
126 project = dao.getProjectDao().find(issue.getProject().getId());
127 }
128 // our object contains more details
129 issue.setProject(project);
130 if (!issue.getResolvedVersions().contains(version)
131 && !issue.getAffectedVersions().contains(version)) {
132 version = null;
133 }
134 updateAttributes();
135 }
137 void syncProject() throws SQLException {
138 final var projectSelection = getParameter(req, Integer.class, "pid");
139 if (projectSelection.isPresent()) {
140 final var selectedProject = dao.getProjectDao().find(projectSelection.get());
141 if (!Objects.equals(selectedProject, project)) {
142 // reset version and issue if project changed
143 version = null;
144 issue = null;
145 }
146 project = selectedProject;
147 } else {
148 project = project == null ? null : dao.getProjectDao().find(project.getId());
149 }
150 }
152 void syncVersion() throws SQLException {
153 final var versionSelection = getParameter(req, Integer.class, "vid");
154 if (versionSelection.isPresent()) {
155 if (versionSelection.get() < 0) {
156 version = null;
157 } else {
158 final var selectedVersion = dao.getVersionDao().find(versionSelection.get());
159 if (!Objects.equals(selectedVersion, version)) {
160 issue = null;
161 }
162 selectVersion(selectedVersion);
163 }
164 } else {
165 version = version == null ? null : dao.getVersionDao().find(version.getId());
166 }
167 }
169 void syncIssue() throws SQLException {
170 final var issueSelection = getParameter(req, Integer.class, "issue");
171 if (issueSelection.isPresent()) {
172 final var selectedIssue = dao.getIssueDao().find(issueSelection.get());
173 dao.getIssueDao().joinVersionInformation(selectedIssue);
174 selectIssue(selectedIssue);
175 } else {
176 issue = issue == null ? null : dao.getIssueDao().find(issue.getId());
177 }
178 }
180 void sync() throws SQLException {
181 project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
182 version = (Version) session.getAttribute(SESSION_ATTR_SELECTED_VERSION);
183 issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
185 syncProject();
186 syncVersion();
187 syncIssue();
189 updateAttributes();
190 }
192 private void updateAttributes() {
193 session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project);
194 session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version);
195 session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue);
196 }
197 }
199 @Override
200 protected String getResourceBundleName() {
201 return "localization.projects";
202 }
205 private static final int BREADCRUMB_LEVEL_ROOT = 0;
206 private static final int BREADCRUMB_LEVEL_PROJECT = 1;
207 private static final int BREADCRUMB_LEVEL_VERSION = 2;
208 private static final int BREADCRUMB_LEVEL_ISSUE_LIST = 3;
209 private static final int BREADCRUMB_LEVEL_ISSUE = 4;
211 /**
212 * Creates the breadcrumb menu.
213 *
214 * @param level the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue)
215 * @param selection the currently selected objects
216 * @return a dynamic breadcrumb menu trying to display as many levels as possible
217 */
218 private List<MenuEntry> getBreadcrumbs(int level, SessionSelection selection) {
219 MenuEntry entry;
221 final var breadcrumbs = new ArrayList<MenuEntry>();
222 entry = new MenuEntry(new ResourceKey("localization.lightpit", "menu.projects"),
223 "projects/");
224 breadcrumbs.add(entry);
225 if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true);
227 if (selection.project != null) {
228 if (selection.project.getId() < 0) {
229 entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"),
230 "projects/edit");
231 } else {
232 entry = new MenuEntry(selection.project.getName(),
233 "projects/view?pid=" + selection.project.getId());
234 }
235 if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true);
236 breadcrumbs.add(entry);
237 }
239 if (selection.version != null) {
240 if (selection.version.getId() < 0) {
241 entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"),
242 "projects/versions/edit");
243 } else {
244 entry = new MenuEntry(selection.version.getName(),
245 "projects/versions/view?vid=" + selection.version.getId());
246 }
247 if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true);
248 breadcrumbs.add(entry);
249 }
251 if (selection.project != null) {
252 String path = "projects/issues/?pid=" + selection.project.getId();
253 if (selection.version != null) {
254 path += "&vid="+selection.version.getId();
255 }
256 entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
257 path);
258 if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true);
259 breadcrumbs.add(entry);
260 }
262 if (selection.issue != null) {
263 if (selection.issue.getId() < 0) {
264 entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"),
265 "projects/issues/edit");
266 } else {
267 entry = new MenuEntry("#" + selection.issue.getId(),
268 // TODO: maybe change link to a view rather than directly opening the editor
269 "projects/issues/edit?issue=" + selection.issue.getId());
270 }
271 if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true);
272 breadcrumbs.add(entry);
273 }
275 return breadcrumbs;
276 }
278 @RequestMapping(method = HttpMethod.GET)
279 public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
280 final var sessionSelection = new SessionSelection(req, dao);
281 sessionSelection.sync();
283 final var projectDao = dao.getProjectDao();
284 final var versionDao = dao.getVersionDao();
286 final var projectList = projectDao.list();
288 final var viewModel = new ProjectIndexView();
289 for (var project : projectList) {
290 final var info = new ProjectInfo(project);
291 info.setVersions(versionDao.list(project));
292 info.setIssueSummary(projectDao.getIssueSummary(project));
293 viewModel.getProjects().add(info);
294 }
296 setViewModel(req, viewModel);
297 setContentPage(req, "projects");
298 setStylesheet(req, "projects");
300 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ROOT, sessionSelection));
302 return ResponseType.HTML;
303 }
305 private ProjectEditView configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
306 final var viewModel = new ProjectEditView();
307 viewModel.setProject(selection.project);
308 viewModel.setUsers(dao.getUserDao().list());
309 setViewModel(req, viewModel);
310 setContentPage(req, "project-form");
311 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
312 return viewModel;
313 }
315 @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
316 public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
317 final var selection = new SessionSelection(req, dao);
318 if (getParameter(req, Integer.class, "pid").isEmpty()) {
319 selection.newProject();
320 } else {
321 selection.sync();
322 }
324 configureEditForm(req, dao, selection);
326 return ResponseType.HTML;
327 }
329 @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
330 public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
332 Project project = new Project(-1);
333 try {
334 project = new Project(getParameter(req, Integer.class, "id").orElseThrow());
335 project.setName(getParameter(req, String.class, "name").orElseThrow());
336 getParameter(req, String.class, "description").ifPresent(project::setDescription);
337 getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
338 getParameter(req, Integer.class, "owner").map(
339 ownerId -> ownerId >= 0 ? new User(ownerId) : null
340 ).ifPresent(project::setOwner);
342 dao.getProjectDao().saveOrUpdate(project);
344 setRedirectLocation(req, "./projects/");
345 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
346 LOG.debug("Successfully updated project {}", project.getName());
347 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
348 LOG.warn("Form validation failure: {}", ex.getMessage());
349 LOG.debug("Details:", ex);
350 final var selection = new SessionSelection(req, dao);
351 selection.project = project;
352 final var vm = configureEditForm(req, dao, selection);
353 vm.setErrorText(ex.getMessage()); // TODO: error text
354 }
356 return ResponseType.HTML;
357 }
359 @RequestMapping(requestPath = "view", method = HttpMethod.GET)
360 public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
361 final var selection = new SessionSelection(req, dao);
362 selection.sync();
364 if (selection.project == null) {
365 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
366 return ResponseType.NONE;
367 }
369 final var versionDao = dao.getVersionDao();
370 final var issueDao = dao.getIssueDao();
372 final var viewModel = new ProjectView(selection.project);
373 final var issues = issueDao.list(selection.project);
374 for (var issue : issues) issueDao.joinVersionInformation(issue);
375 viewModel.setIssues(issues);
376 viewModel.setVersions(versionDao.list(selection.project));
377 viewModel.updateVersionInfo();
378 setViewModel(req, viewModel);
380 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
381 setContentPage(req, "project-details");
382 setStylesheet(req, "projects");
384 return ResponseType.HTML;
385 }
387 @RequestMapping(requestPath = "versions/view", method = HttpMethod.GET)
388 public ResponseType viewVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
389 final var selection = new SessionSelection(req, dao);
390 selection.sync();
391 if (selection.version == null) {
392 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
393 return ResponseType.NONE;
394 }
396 final var issueDao = dao.getIssueDao();
397 final var viewModel = new VersionView(selection.version);
398 final var issues = issueDao.list(selection.version);
399 for (var issue : issues) issueDao.joinVersionInformation(issue);
400 viewModel.setIssues(issues);
401 setViewModel(req, viewModel);
403 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
404 setContentPage(req, "version");
405 setStylesheet(req, "projects");
407 return ResponseType.HTML;
408 }
410 private VersionEditView configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
411 final var viewModel = new VersionEditView(selection.version);
412 if (selection.version.getProject() == null) {
413 viewModel.setProjects(dao.getProjectDao().list());
414 }
415 setViewModel(req, viewModel);
416 setContentPage(req, "version-form");
417 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
418 return viewModel;
419 }
421 @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
422 public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
423 final var selection = new SessionSelection(req, dao);
424 if (getParameter(req, Integer.class, "vid").isEmpty()) {
425 selection.newVersion();
426 } else {
427 selection.sync();
428 }
430 configureEditVersionForm(req, dao, selection);
432 return ResponseType.HTML;
433 }
435 @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
436 public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
438 var version = new Version(-1);
439 try {
440 version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
441 version.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
442 version.setName(getParameter(req, String.class, "name").orElseThrow());
443 getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
444 version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
445 dao.getVersionDao().saveOrUpdate(version);
447 // specifying the pid parameter will purposely reset the session selected version!
448 setRedirectLocation(req, "./projects/view?pid="+version.getProject().getId());
449 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
450 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
451 LOG.warn("Form validation failure: {}", ex.getMessage());
452 LOG.debug("Details:", ex);
453 final var selection = new SessionSelection(req, dao);
454 selection.selectVersion(version);
455 final var viewModel = configureEditVersionForm(req, dao, selection);
456 // TODO: set Error Text
457 }
459 return ResponseType.HTML;
460 }
462 private IssueEditView configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
463 final var viewModel = new IssueEditView(selection.issue);
465 if (selection.issue.getProject() == null) {
466 viewModel.setProjects(dao.getProjectDao().list());
467 } else {
468 viewModel.setVersions(dao.getVersionDao().list(selection.issue.getProject()));
469 }
470 viewModel.setUsers(dao.getUserDao().list());
471 setViewModel(req, viewModel);
473 setContentPage(req, "issue-form");
474 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection));
475 return viewModel;
476 }
478 @RequestMapping(requestPath = "issues/", method = HttpMethod.GET)
479 public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
480 final var selection = new SessionSelection(req, dao);
481 selection.sync();
482 if (selection.project == null) {
483 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
484 return ResponseType.NONE;
485 }
487 final var viewModel = new IssuesView();
488 viewModel.setProject(selection.project);
489 if (selection.version == null) {
490 viewModel.setIssues(dao.getIssueDao().list(selection.project));
491 } else {
492 viewModel.setVersion(selection.version);
493 viewModel.setIssues(dao.getIssueDao().list(selection.version));
494 }
495 setViewModel(req, viewModel);
497 setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, selection));
498 setContentPage(req, "issues");
499 setStylesheet(req, "projects");
501 return ResponseType.HTML;
502 }
504 @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
505 public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
506 final var selection = new SessionSelection(req, dao);
507 if (getParameter(req, Integer.class, "issue").isEmpty()) {
508 selection.newIssue();
509 } else {
510 selection.sync();
511 }
513 configureEditIssueForm(req, dao, selection);
515 return ResponseType.HTML;
516 }
518 @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
519 public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
521 Issue issue = new Issue(-1);
522 try {
523 issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
524 issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
525 getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
526 getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
527 issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
528 getParameter(req, Integer.class, "assignee").map(
529 userid -> userid >= 0 ? new User(userid) : null
530 ).ifPresent(issue::setAssignee);
531 getParameter(req, String.class, "description").ifPresent(issue::setDescription);
532 getParameter(req, Date.class, "eta").ifPresent(issue::setEta);
534 getParameter(req, Integer[].class, "affected")
535 .map(Stream::of)
536 .map(stream ->
537 stream.map(Version::new).collect(Collectors.toList())
538 ).ifPresent(issue::setAffectedVersions);
539 getParameter(req, Integer[].class, "resolved")
540 .map(Stream::of)
541 .map(stream ->
542 stream.map(Version::new).collect(Collectors.toList())
543 ).ifPresent(issue::setResolvedVersions);
545 dao.getIssueDao().saveOrUpdate(issue);
547 // specifying the issue parameter keeps the edited issue as breadcrumb
548 setRedirectLocation(req, "./projects/issues/?issue="+issue.getId());
549 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
550 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
551 // TODO: set request attribute with error text
552 LOG.warn("Form validation failure: {}", ex.getMessage());
553 LOG.debug("Details:", ex);
554 final var selection = new SessionSelection(req, dao);
555 selection.selectIssue(issue);
556 final var viewModel = configureEditIssueForm(req, dao, selection);
557 // TODO: set Error Text
558 }
560 return ResponseType.HTML;
561 }
562 }