Sat, 29 Aug 2020 17:32:59 +0200
fixes misbehavior when a non-existing project ID is selected
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 java.io.IOException;
43 import java.sql.Date;
44 import java.sql.SQLException;
45 import java.util.ArrayList;
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 String queryParams(Project p, Version v) {
72 return String.format("pid=%d&vid=%d",
73 p == null ? -1 : p.getId(),
74 v == null ? -1 : v.getId()
75 );
76 }
78 /**
79 * Creates the navigation menu.
80 *
81 * @param req the servlet request
82 * @param viewModel the current view model
83 */
84 private void setNavigationMenu(HttpServletRequest req, ProjectView viewModel) {
85 final Project selectedProject = Optional.ofNullable(viewModel.getProjectInfo()).map(ProjectInfo::getProject).orElse(null);
87 final var navigation = new ArrayList<MenuEntry>();
89 for (ProjectInfo plistInfo : viewModel.getProjectList()) {
90 final var proj = plistInfo.getProject();
91 final var projEntry = new MenuEntry(
92 proj.getName(),
93 "projects/view?" + queryParams(proj, null)
94 );
95 navigation.add(projEntry);
96 if (proj.equals(selectedProject)) {
97 final var projInfo = viewModel.getProjectInfo();
98 projEntry.setActive(true);
100 // ****************
101 // Versions Section
102 // ****************
103 {
104 final var entry = new MenuEntry(1,
105 new ResourceKey(getResourceBundleName(), "menu.versions"),
106 "projects/view?" + queryParams(proj, null)
107 );
108 navigation.add(entry);
109 }
111 final var level2 = new ArrayList<MenuEntry>();
112 {
113 final var entry = new MenuEntry(
114 new ResourceKey(getResourceBundleName(), "filter.none"),
115 "projects/view?" + queryParams(proj, null)
116 );
117 if (viewModel.getVersionFilter() == null) entry.setActive(true);
118 level2.add(entry);
119 }
121 for (Version version : projInfo.getVersions()) {
122 final var entry = new MenuEntry(
123 version.getName(),
124 "projects/view?" + queryParams(proj, version)
125 );
126 if (version.equals(viewModel.getVersionFilter())) entry.setActive(true);
127 level2.add(entry);
128 }
130 level2.forEach(e -> e.setLevel(2));
131 navigation.addAll(level2);
132 }
133 }
135 setNavigationMenu(req, navigation);
136 }
138 private int syncParamWithSession(HttpServletRequest req, String param, String attr) {
139 final var session = req.getSession();
140 final var idParam = getParameter(req, Integer.class, param);
141 final int id;
142 if (idParam.isPresent()) {
143 id = idParam.get();
144 session.setAttribute(attr, id);
145 } else {
146 id = Optional.ofNullable(session.getAttribute(attr)).map(x->(Integer)x).orElse(-1);
147 }
148 return id;
149 }
151 private void populate(ProjectView viewModel, HttpServletRequest req, DataAccessObjects dao) throws SQLException {
152 final var projectDao = dao.getProjectDao();
153 final var versionDao = dao.getVersionDao();
155 projectDao.list().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add);
157 // Select Project
158 final int pid = syncParamWithSession(req, PARAMETER_SELECTED_PROJECT, SESSION_ATTR_SELECTED_PROJECT);
159 if (pid >= 0) {
160 final var project = projectDao.find(pid);
161 if (project == null) {
162 req.setAttribute(SESSION_ATTR_SELECTED_PROJECT, -1);
163 } else {
164 final var info = new ProjectInfo(project);
165 info.setVersions(versionDao.list(project));
166 info.setIssueSummary(projectDao.getIssueSummary(project));
167 viewModel.setProjectInfo(info);
168 }
169 }
171 // Select Version
172 final int vid = syncParamWithSession(req, PARAMETER_SELECTED_VERSION, SESSION_ATTR_SELECTED_VERSION);
173 if (vid >= 0) {
174 viewModel.setVersionFilter(versionDao.find(vid));
175 }
176 }
178 private ResponseType forwardView(HttpServletRequest req, ProjectView viewModel, String name) {
179 setViewModel(req, viewModel);
180 setContentPage(req, name);
181 setStylesheet(req, "projects");
182 setNavigationMenu(req, viewModel);
183 return ResponseType.HTML;
184 }
186 @RequestMapping(method = HttpMethod.GET)
187 public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
188 final var viewModel = new ProjectView();
189 populate(viewModel, req, dao);
191 final var projectDao = dao.getProjectDao();
192 final var versionDao = dao.getVersionDao();
194 for (var info : viewModel.getProjectList()) {
195 info.setVersions(versionDao.list(info.getProject()));
196 info.setIssueSummary(projectDao.getIssueSummary(info.getProject()));
197 }
199 return forwardView(req, viewModel, "projects");
200 }
202 private void configure(ProjectEditView viewModel, Project project, DataAccessObjects dao) throws SQLException {
203 viewModel.setProject(project);
204 viewModel.setUsers(dao.getUserDao().list());
205 }
207 @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
208 public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
209 final var viewModel = new ProjectEditView();
210 populate(viewModel, req, dao);
212 final var project = Optional.ofNullable(viewModel.getProjectInfo())
213 .map(ProjectInfo::getProject)
214 .orElse(new Project(-1));
215 configure(viewModel, project, dao);
217 return forwardView(req, viewModel, "project-form");
218 }
220 @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
221 public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
223 Project project = new Project(-1);
224 try {
225 project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
226 project.setName(getParameter(req, String.class, "name").orElseThrow());
227 getParameter(req, String.class, "description").ifPresent(project::setDescription);
228 getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
229 getParameter(req, Integer.class, "owner").map(
230 ownerId -> ownerId >= 0 ? new User(ownerId) : null
231 ).ifPresent(project::setOwner);
233 dao.getProjectDao().saveOrUpdate(project);
235 setRedirectLocation(req, "./projects/view?pid="+project.getId());
236 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
237 LOG.debug("Successfully updated project {}", project.getName());
239 return ResponseType.HTML;
240 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
241 LOG.warn("Form validation failure: {}", ex.getMessage());
242 LOG.debug("Details:", ex);
243 final var viewModel = new ProjectEditView();
244 populate(viewModel, req, dao);
245 configure(viewModel, project, dao);
246 // TODO: error text
247 return forwardView(req, viewModel, "project-form");
248 }
249 }
251 @RequestMapping(requestPath = "view", method = HttpMethod.GET)
252 public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
253 final var viewModel = new ProjectDetailsView();
254 populate(viewModel, req, dao);
256 if (viewModel.getProjectInfo() == null) {
257 resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
258 return ResponseType.NONE;
259 }
261 final var issueDao = dao.getIssueDao();
263 final var version = viewModel.getVersionFilter();
265 final var detailView = viewModel.getProjectDetails();
266 final var issues = issueDao.list(version);
267 for (var issue : issues) issueDao.joinVersionInformation(issue);
268 detailView.updateDetails(issues, version);
270 return forwardView(req, viewModel, "project-details");
271 }
273 @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
274 public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
275 final var viewModel = new VersionEditView();
276 populate(viewModel, req, dao);
278 if (viewModel.getVersionFilter() == null) {
279 viewModel.setVersion(new Version(-1));
280 } else {
281 viewModel.setVersion(viewModel.getVersionFilter());
282 }
284 return forwardView(req, viewModel, "version-form");
285 }
287 @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
288 public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
290 var version = new Version(-1);
291 try {
292 version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
293 version.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
294 version.setName(getParameter(req, String.class, "name").orElseThrow());
295 getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
296 version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
297 dao.getVersionDao().saveOrUpdate(version);
299 // specifying the pid parameter will purposely reset the session selected version!
300 setRedirectLocation(req, "./projects/view?pid=" + version.getProject().getId());
301 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
302 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
303 LOG.warn("Form validation failure: {}", ex.getMessage());
304 LOG.debug("Details:", ex);
305 final var viewModel = new VersionEditView();
306 populate(viewModel, req, dao);
307 viewModel.setVersion(version);
308 // TODO: set Error Text
309 return forwardView(req, viewModel, "version-form");
310 }
312 return ResponseType.HTML;
313 }
315 private void configure(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
316 issue.setProject(viewModel.getProjectInfo().getProject());
317 viewModel.setIssue(issue);
318 viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
319 viewModel.setUsers(dao.getUserDao().list());
320 }
322 @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
323 public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
324 final var viewModel = new IssueEditView();
326 final var issueParam = getParameter(req, Integer.class, "issue");
327 if (issueParam.isPresent()) {
328 final var issueDao = dao.getIssueDao();
329 final var issue = issueDao.find(issueParam.get());
330 issueDao.joinVersionInformation(issue);
331 req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, issue.getProject().getId());
332 populate(viewModel, req, dao);
333 configure(viewModel, issue, dao);
334 } else {
335 populate(viewModel, req, dao);
336 configure(viewModel, new Issue(-1), dao);
337 }
339 return forwardView(req, viewModel, "issue-form");
340 }
342 @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
343 public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
344 Issue issue = new Issue(-1);
345 try {
346 issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
347 issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
348 getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
349 getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
350 issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
351 getParameter(req, Integer.class, "assignee").map(
352 userid -> userid >= 0 ? new User(userid) : null
353 ).ifPresent(issue::setAssignee);
354 getParameter(req, String.class, "description").ifPresent(issue::setDescription);
355 getParameter(req, Date.class, "eta").ifPresent(issue::setEta);
357 getParameter(req, Integer[].class, "affected")
358 .map(Stream::of)
359 .map(stream ->
360 stream.map(Version::new).collect(Collectors.toList())
361 ).ifPresent(issue::setAffectedVersions);
362 getParameter(req, Integer[].class, "resolved")
363 .map(Stream::of)
364 .map(stream ->
365 stream.map(Version::new).collect(Collectors.toList())
366 ).ifPresent(issue::setResolvedVersions);
368 dao.getIssueDao().saveOrUpdate(issue);
370 // specifying the issue parameter keeps the edited issue as menu item
371 setRedirectLocation(req, "./projects/view?pid=" + issue.getProject().getId());
372 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
373 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
374 // TODO: set request attribute with error text
375 LOG.warn("Form validation failure: {}", ex.getMessage());
376 LOG.debug("Details:", ex);
377 final var viewModel = new IssueEditView();
378 configure(viewModel, issue, dao);
379 // TODO: set Error Text
380 return forwardView(req, viewModel, "issue-form");
381 }
383 return ResponseType.HTML;
384 }
385 }