Fri, 09 Oct 2020 19:07:05 +0200
adds issue comments
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 de.uapcore.lightpit.viewmodel.util.IssueSorter;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import javax.servlet.annotation.WebServlet;
41 import javax.servlet.http.HttpServletRequest;
42 import javax.servlet.http.HttpServletResponse;
43 import java.io.IOException;
44 import java.sql.Date;
45 import java.sql.SQLException;
46 import java.util.NoSuchElementException;
47 import java.util.Optional;
48 import java.util.stream.Collectors;
49 import java.util.stream.Stream;
51 import static de.uapcore.lightpit.Functions.fqn;
53 @WebServlet(
54 name = "ProjectsModule",
55 urlPatterns = "/projects/*"
56 )
57 public final class ProjectsModule extends AbstractLightPITServlet {
59 private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
61 private static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
62 private static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
63 private static final String PARAMETER_SELECTED_PROJECT = "pid";
64 private static final String PARAMETER_SELECTED_VERSION = "vid";
66 @Override
67 protected String getResourceBundleName() {
68 return "localization.projects";
69 }
71 private int syncParamWithSession(HttpServletRequest req, String param, String attr) {
72 final var session = req.getSession();
73 final var idParam = getParameter(req, Integer.class, param);
74 final int id;
75 if (idParam.isPresent()) {
76 id = idParam.get();
77 session.setAttribute(attr, id);
78 } else {
79 id = Optional.ofNullable(session.getAttribute(attr)).map(x->(Integer)x).orElse(-1);
80 }
81 return id;
82 }
84 private void populate(ProjectView viewModel, HttpServletRequest req, DataAccessObjects dao) throws SQLException {
85 final var projectDao = dao.getProjectDao();
86 final var versionDao = dao.getVersionDao();
88 projectDao.list().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add);
90 // Select Project
91 final int pid = syncParamWithSession(req, PARAMETER_SELECTED_PROJECT, SESSION_ATTR_SELECTED_PROJECT);
92 if (pid >= 0) {
93 final var project = projectDao.find(pid);
94 if (project == null) {
95 req.setAttribute(SESSION_ATTR_SELECTED_PROJECT, -1);
96 } else {
97 final var info = new ProjectInfo(project);
98 info.setVersions(versionDao.list(project));
99 info.setIssueSummary(projectDao.getIssueSummary(project));
100 viewModel.setProjectInfo(info);
101 }
102 }
104 // Select Version
105 final int vid = syncParamWithSession(req, PARAMETER_SELECTED_VERSION, SESSION_ATTR_SELECTED_VERSION);
106 if (vid >= 0) {
107 viewModel.setVersionFilter(versionDao.find(vid));
108 }
109 }
111 private ResponseType forwardView(HttpServletRequest req, ProjectView viewModel, String name) {
112 setViewModel(req, viewModel);
113 setContentPage(req, name);
114 setStylesheet(req, "projects");
115 setNavigationMenu(req, "project-navmenu");
116 return ResponseType.HTML;
117 }
119 @RequestMapping(method = HttpMethod.GET)
120 public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
121 final var viewModel = new ProjectView();
122 populate(viewModel, req, dao);
124 final var projectDao = dao.getProjectDao();
125 final var versionDao = dao.getVersionDao();
127 for (var info : viewModel.getProjectList()) {
128 info.setVersions(versionDao.list(info.getProject()));
129 info.setIssueSummary(projectDao.getIssueSummary(info.getProject()));
130 }
132 return forwardView(req, viewModel, "projects");
133 }
135 private void configure(ProjectEditView viewModel, Project project, DataAccessObjects dao) throws SQLException {
136 viewModel.setProject(project);
137 viewModel.setUsers(dao.getUserDao().list());
138 }
140 @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
141 public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
142 final var viewModel = new ProjectEditView();
143 populate(viewModel, req, dao);
145 final var project = Optional.ofNullable(viewModel.getProjectInfo())
146 .map(ProjectInfo::getProject)
147 .orElse(new Project(-1));
148 configure(viewModel, project, dao);
150 return forwardView(req, viewModel, "project-form");
151 }
153 @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
154 public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
156 Project project = new Project(-1);
157 try {
158 project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
159 project.setName(getParameter(req, String.class, "name").orElseThrow());
160 getParameter(req, String.class, "description").ifPresent(project::setDescription);
161 getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
162 getParameter(req, Integer.class, "owner").map(
163 ownerId -> ownerId >= 0 ? new User(ownerId) : null
164 ).ifPresent(project::setOwner);
166 dao.getProjectDao().saveOrUpdate(project);
168 setRedirectLocation(req, "./projects/");
169 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
170 LOG.debug("Successfully updated project {}", project.getName());
172 return ResponseType.HTML;
173 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
174 LOG.warn("Form validation failure: {}", ex.getMessage());
175 LOG.debug("Details:", ex);
176 final var viewModel = new ProjectEditView();
177 populate(viewModel, req, dao);
178 configure(viewModel, project, dao);
179 // TODO: error text
180 return forwardView(req, viewModel, "project-form");
181 }
182 }
184 @RequestMapping(requestPath = "view", method = HttpMethod.GET)
185 public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
186 final var viewModel = new ProjectDetailsView();
187 populate(viewModel, req, dao);
189 if (viewModel.getProjectInfo() == null) {
190 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
191 return ResponseType.NONE;
192 }
194 final var issueDao = dao.getIssueDao();
196 final var version = viewModel.getVersionFilter();
198 final var detailView = viewModel.getProjectDetails();
199 final var issues = issueDao.list(version);
200 for (var issue : issues) issueDao.joinVersionInformation(issue);
201 issues.sort(new IssueSorter(
202 new IssueSorter.Criteria(IssueSorter.Field.PHASE, true),
203 new IssueSorter.Criteria(IssueSorter.Field.ETA, true),
204 new IssueSorter.Criteria(IssueSorter.Field.UPDATED, false)
205 ));
206 detailView.updateDetails(issues, version);
208 return forwardView(req, viewModel, "project-details");
209 }
211 @RequestMapping(requestPath = "versions", method = HttpMethod.GET)
212 public ResponseType versions(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
213 final var viewModel = new VersionsView();
214 populate(viewModel, req, dao);
215 viewModel.setVersionFilter(null);
217 final var projectInfo = viewModel.getProjectInfo();
218 if (projectInfo == null) {
219 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
220 return ResponseType.NONE;
221 }
223 final var issueDao = dao.getIssueDao();
224 final var issues = issueDao.list(projectInfo.getProject());
225 for (var issue : issues) issueDao.joinVersionInformation(issue);
226 viewModel.update(projectInfo.getVersions(), issues);
228 return forwardView(req, viewModel, "versions");
229 }
231 @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
232 public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
233 final var viewModel = new VersionEditView();
234 populate(viewModel, req, dao);
236 if (viewModel.getProjectInfo() == null) {
237 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
238 return ResponseType.NONE;
239 }
241 if (viewModel.getVersionFilter() == null) {
242 final var version = new Version(-1);
243 version.setProject(viewModel.getProjectInfo().getProject());
244 viewModel.setVersion(version);
245 } else {
246 viewModel.setVersion(viewModel.getVersionFilter());
247 }
249 return forwardView(req, viewModel, "version-form");
250 }
252 @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
253 public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
255 var version = new Version(-1);
256 try {
257 version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
258 version.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
259 version.setName(getParameter(req, String.class, "name").orElseThrow());
260 getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
261 version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
262 dao.getVersionDao().saveOrUpdate(version);
264 // specifying the pid parameter will purposely reset the session selected version!
265 setRedirectLocation(req, "./projects/versions?pid=" + version.getProject().getId());
266 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
267 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
268 LOG.warn("Form validation failure: {}", ex.getMessage());
269 LOG.debug("Details:", ex);
270 final var viewModel = new VersionEditView();
271 populate(viewModel, req, dao);
272 viewModel.setVersion(version);
273 // TODO: set Error Text
274 return forwardView(req, viewModel, "version-form");
275 }
277 return ResponseType.HTML;
278 }
280 private void configure(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
281 issue.setProject(viewModel.getProjectInfo().getProject());
282 viewModel.setIssue(issue);
283 viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
284 viewModel.setUsers(dao.getUserDao().list());
285 if (issue.getId() >= 0) {
286 viewModel.setComments(dao.getIssueDao().listComments(issue));
287 }
288 }
290 @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
291 public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
292 final var viewModel = new IssueEditView();
293 populate(viewModel, req, dao);
295 final var issueParam = getParameter(req, Integer.class, "issue");
296 if (issueParam.isPresent()) {
297 final var issueDao = dao.getIssueDao();
298 final var issue = issueDao.find(issueParam.get());
299 issueDao.joinVersionInformation(issue);
300 req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, issue.getProject().getId());
301 configure(viewModel, issue, dao);
302 } else {
303 configure(viewModel, new Issue(-1), dao);
304 }
306 return forwardView(req, viewModel, "issue-form");
307 }
309 @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
310 public ResponseType commitIssue(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
311 Issue issue = new Issue(-1);
312 try {
313 issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
314 issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
315 getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
316 getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
317 issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
318 getParameter(req, Integer.class, "assignee").map(
319 userid -> userid >= 0 ? new User(userid) : null
320 ).ifPresent(issue::setAssignee);
321 getParameter(req, String.class, "description").ifPresent(issue::setDescription);
322 getParameter(req, Date.class, "eta").ifPresent(issue::setEta);
324 getParameter(req, Integer[].class, "affected")
325 .map(Stream::of)
326 .map(stream ->
327 stream.map(Version::new).collect(Collectors.toList())
328 ).ifPresent(issue::setAffectedVersions);
329 getParameter(req, Integer[].class, "resolved")
330 .map(Stream::of)
331 .map(stream ->
332 stream.map(Version::new).collect(Collectors.toList())
333 ).ifPresent(issue::setResolvedVersions);
335 dao.getIssueDao().saveOrUpdate(issue);
337 // specifying the issue parameter keeps the edited issue as menu item
338 setRedirectLocation(req, "./projects/view?pid=" + issue.getProject().getId());
339 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
341 return ResponseType.HTML;
342 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
343 // TODO: set request attribute with error text
344 LOG.warn("Form validation failure: {}", ex.getMessage());
345 LOG.debug("Details:", ex);
346 final var viewModel = new IssueEditView();
347 populate(viewModel, req, dao);
348 configure(viewModel, issue, dao);
349 // TODO: set Error Text
350 return forwardView(req, viewModel, "issue-form");
351 }
352 }
354 @RequestMapping(requestPath = "issues/comment", method = HttpMethod.POST)
355 public ResponseType commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
356 final var issueIdParam = getParameter(req, Integer.class, "issueid");
357 if (issueIdParam.isEmpty()) {
358 resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form.");
359 return ResponseType.NONE;
360 }
361 final var issue = new Issue(issueIdParam.get());
362 try {
363 final var issueComment = new IssueComment(getParameter(req, Integer.class, "commentid").orElse(-1), issue);
364 issueComment.setComment(getParameter(req, String.class, "comment").orElse(""));
366 if (issueComment.getComment().isBlank()) {
367 throw new IllegalArgumentException("comment.null");
368 }
370 LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId());
371 if (req.getRemoteUser() != null) {
372 dao.getUserDao().findByUsername(req.getRemoteUser()).ifPresent(issueComment::setAuthor);
373 }
375 dao.getIssueDao().saveComment(issueComment);
377 // specifying the issue parameter keeps the edited issue as menu item
378 setRedirectLocation(req, "./projects/issues/edit?issue=" + issue.getId());
379 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
381 return ResponseType.HTML;
382 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
383 // TODO: set request attribute with error text
384 LOG.warn("Form validation failure: {}", ex.getMessage());
385 LOG.debug("Details:", ex);
386 final var viewModel = new IssueEditView();
387 populate(viewModel, req, dao);
388 configure(viewModel, issue, dao);
389 // TODO: set Error Text
390 return forwardView(req, viewModel, "issue-form");
391 }
392 }
393 }