Sat, 30 May 2020 18:05:06 +0200
adds version selection in issue editor
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sat May 30 15:28:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sat May 30 18:05:06 2020 +0200 @@ -39,10 +39,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.lang.reflect.*; import java.sql.Connection; import java.sql.SQLException; import java.util.*; @@ -280,30 +277,44 @@ * @return the parameter value or an empty optional, if no parameter with the specified name was found */ protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { - final String paramValue = req.getParameter(name); - if (paramValue == null) return Optional.empty(); - if (clazz.equals(Boolean.class)) { - if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) { - return Optional.of((T)Boolean.FALSE); - } else { - return Optional.of((T)Boolean.TRUE); + if (clazz.isArray()) { + final String[] paramValues = req.getParameterValues(name); + int len = paramValues == null ? 0 : paramValues.length; + final var array = (T) Array.newInstance(clazz.getComponentType(), len); + for (int i = 0 ; i < len ; i++) { + try { + final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class); + Array.set(array, i, ctor.newInstance(paramValues[i])); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + return Optional.of(array); + } else { + final String paramValue = req.getParameter(name); + if (paramValue == null) return Optional.empty(); + if (clazz.equals(Boolean.class)) { + if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) { + return Optional.of((T) Boolean.FALSE); + } else { + return Optional.of((T) Boolean.TRUE); + } + } + if (clazz.equals(String.class)) return Optional.of((T) paramValue); + if (java.sql.Date.class.isAssignableFrom(clazz)) { + try { + return Optional.of((T) java.sql.Date.valueOf(paramValue)); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } + try { + final Constructor<T> ctor = clazz.getConstructor(String.class); + return Optional.of(ctor.newInstance(paramValue)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); } } - if (clazz.equals(String.class)) return Optional.of((T) paramValue); - if (java.sql.Date.class.isAssignableFrom(clazz)) { - try { - return Optional.of((T)java.sql.Date.valueOf(paramValue)); - } catch (IllegalArgumentException ex) { - return Optional.empty(); - } - } - try { - final Constructor<T> ctor = clazz.getConstructor(String.class); - return Optional.of(ctor.newInstance(paramValue)); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } /**
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Sat May 30 15:28:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Sat May 30 18:05:06 2020 +0200 @@ -43,22 +43,27 @@ public final class PGIssueDao implements IssueDao { - private final PreparedStatement insert, update, list, find, affectedVersions, scheduledVersions, resolvedVersions; + private final PreparedStatement insert, update, list, find; + private final PreparedStatement affectedVersions, scheduledVersions, resolvedVersions; + private final PreparedStatement clearAffected, clearScheduled, clearResolved; + private final PreparedStatement insertAffected, insertScheduled, insertResolved; public PGIssueDao(Connection connection) throws SQLException { list = connection.prepareStatement( - "select issueid, project, status, category, subject, description, " + + "select issueid, project, p.name as projectname, status, category, subject, i.description, " + "userid, username, givenname, lastname, mail, " + "created, updated, eta " + - "from lpit_issue " + + "from lpit_issue i " + + "left join lpit_project p on project = projectid " + "left join lpit_user on userid = assignee " + "where project = ? "); find = connection.prepareStatement( - "select issueid, project, status, category, subject, description, " + + "select issueid, project, p.name as projectname, status, category, subject, i.description, " + "userid, username, givenname, lastname, mail, " + "created, updated, eta " + - "from lpit_issue " + + "from lpit_issue i " + + "left join lpit_project p on project = projectid " + "left join lpit_user on userid = assignee " + "where issueid = ? "); @@ -72,25 +77,31 @@ ); affectedVersions = connection.prepareStatement( - "select v.versionid, v.name, v.status, v.ordinal " + - "from lpit_version v join lpit_issue_affected_version using (versionid) " + + "select versionid, name, status, ordinal " + + "from lpit_version join lpit_issue_affected_version using (versionid) " + "where issueid = ? " + - "order by v.ordinal, v.name" + "order by ordinal, name" ); + clearAffected = connection.prepareStatement("delete from lpit_issue_affected_version where issueid = ?"); + insertAffected = connection.prepareStatement("insert into lpit_issue_affected_version (issueid, versionid) values (?,?)"); scheduledVersions = connection.prepareStatement( - "select v.versionid, v.name, v.status, v.ordinal " + - "from lpit_version v join lpit_issue_scheduled_version using (versionid) " + + "select versionid, name, status, ordinal " + + "from lpit_version join lpit_issue_scheduled_version using (versionid) " + "where issueid = ? " + - "order by v.ordinal, v.name" + "order by ordinal, name" ); + clearScheduled = connection.prepareStatement("delete from lpit_issue_scheduled_version where issueid = ?"); + insertScheduled = connection.prepareStatement("insert into lpit_issue_scheduled_version (issueid, versionid) values (?,?)"); resolvedVersions = connection.prepareStatement( - "select v.versionid, v.name, v.status, v.ordinal " + + "select versionid, name, status, ordinal " + "from lpit_version v join lpit_issue_resolved_version using (versionid) " + "where issueid = ? " + - "order by v.ordinal, v.name" + "order by ordinal, name" ); + clearResolved = connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?"); + insertResolved = connection.prepareStatement("insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)"); } private User obtainAssignee(ResultSet result) throws SQLException { @@ -109,6 +120,7 @@ private Issue mapColumns(ResultSet result) throws SQLException { final var project = new Project(result.getInt("project")); + project.setName(result.getString("projectname")); final var issue = new Issue(result.getInt("issueid"), project); issue.setStatus(IssueStatus.valueOf(result.getString("status"))); issue.setCategory(IssueCategory.valueOf(result.getString("category"))); @@ -122,13 +134,37 @@ } private Version mapVersion(ResultSet result, Project project) throws SQLException { - final var version = new Version(result.getInt("v.versionid"), project); - version.setName(result.getString("v.name")); - version.setOrdinal(result.getInt("v.ordinal")); - version.setStatus(VersionStatus.valueOf(result.getString("v.status"))); + final var version = new Version(result.getInt("versionid"), project); + version.setName(result.getString("name")); + version.setOrdinal(result.getInt("ordinal")); + version.setStatus(VersionStatus.valueOf(result.getString("status"))); return version; } + private void updateVersionLists(Issue instance) throws SQLException { + clearAffected.setInt(1, instance.getId()); + clearScheduled.setInt(1, instance.getId()); + clearResolved.setInt(1, instance.getId()); + insertAffected.setInt(1, instance.getId()); + insertScheduled.setInt(1, instance.getId()); + insertResolved.setInt(1, instance.getId()); + clearAffected.executeUpdate(); + clearScheduled.executeUpdate(); + clearResolved.executeUpdate(); + for (Version v : instance.getAffectedVersions()) { + insertAffected.setInt(2, v.getId()); + insertAffected.executeUpdate(); + } + for (Version v : instance.getScheduledVersions()) { + insertScheduled.setInt(2, v.getId()); + insertScheduled.executeUpdate(); + } + for (Version v : instance.getResolvedVersions()) { + insertResolved.setInt(2, v.getId()); + insertResolved.executeUpdate(); + } + } + @Override public void save(Issue instance) throws SQLException { Objects.requireNonNull(instance.getSubject()); @@ -144,6 +180,7 @@ final var rs = insert.executeQuery(); rs.next(); instance.setId(rs.getInt(1)); + updateVersionLists(instance); } @Override @@ -157,7 +194,13 @@ setForeignKeyOrNull(update, 5, instance.getAssignee(), User::getId); setDateOrNull(update, 6, instance.getEta()); update.setInt(7, instance.getId()); - return update.executeUpdate() > 0; + boolean success = update.executeUpdate() > 0; + if (success) { + updateVersionLists(instance); + return true; + } else { + return false; + } } @Override
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Sat May 30 15:28:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Sat May 30 18:05:06 2020 +0200 @@ -46,14 +46,16 @@ public PGVersionDao(Connection connection) throws SQLException { list = connection.prepareStatement( - "select versionid, project, name, ordinal, status " + - "from lpit_version " + + "select versionid, project, p.name as projectname, v.name, ordinal, status " + + "from lpit_version v " + + "join lpit_project p on v.project = p.projectid " + "where project = ? " + - "order by ordinal desc, lower(name) desc"); + "order by ordinal desc, lower(v.name) desc"); find = connection.prepareStatement( - "select versionid, project, name, ordinal, status " + - "from lpit_version " + + "select versionid, project, p.name as projectname, v.name, ordinal, status " + + "from lpit_version v " + + "join lpit_project p on v.project = p.projectid " + "where versionid = ?"); insert = connection.prepareStatement( @@ -88,6 +90,7 @@ private Version mapColumns(ResultSet result) throws SQLException { final var project = new Project(result.getInt("project")); + project.setName(result.getString("projectname")); final var version = new Version(result.getInt("versionid"), project); version.setName(result.getString("name")); version.setOrdinal(result.getInt("ordinal"));
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Sat May 30 15:28:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Sat May 30 18:05:06 2020 +0200 @@ -42,10 +42,9 @@ 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.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static de.uapcore.lightpit.Functions.fqn; @@ -109,14 +108,14 @@ } void selectVersion(Version version) { - if (!Objects.equals(project, version.getProject())) throw new AssertionError("Nice, you implemented a bug!"); + this.project = version.getProject(); this.version = version; this.issue = null; updateAttributes(); } void selectIssue(Issue issue) { - if (!Objects.equals(issue.getProject(), project)) throw new AssertionError("Nice, you implemented a bug!"); + this.project = issue.getProject(); this.issue = issue; this.version = null; updateAttributes(); @@ -375,7 +374,16 @@ } private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { - req.setAttribute("projects", dao.getProjectDao().list()); + + if (selection.issue.getProject() == null || selection.issue.getProject().getId() < 0) { + req.setAttribute("projects", dao.getProjectDao().list()); + req.setAttribute("versions", Collections.<Version>emptyList()); + } else { + req.setAttribute("projects", Collections.<Project>emptyList()); + req.setAttribute("versions", dao.getVersionDao().list(selection.issue.getProject())); + } + + dao.getIssueDao().joinVersionInformation(selection.issue); req.setAttribute("issue", selection.issue); req.setAttribute("issueStatusEnum", IssueStatus.values()); req.setAttribute("issueCategoryEnum", IssueCategory.values()); @@ -428,6 +436,23 @@ ).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, sessionSelection.project)).collect(Collectors.toList()) + ).ifPresent(issue::setAffectedVersions); + getParameter(req, Integer[].class, "scheduled") + .map(Stream::of) + .map(stream -> + stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList()) + ).ifPresent(issue::setScheduledVersions); + getParameter(req, Integer[].class, "resolved") + .map(Stream::of) + .map(stream -> + stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList()) + ).ifPresent(issue::setResolvedVersions); + dao.getIssueDao().saveOrUpdate(issue); // specifying the issue parameter keeps the edited issue as breadcrumb
--- a/src/main/resources/localization/projects.properties Sat May 30 15:28:27 2020 +0200 +++ b/src/main/resources/localization/projects.properties Sat May 30 18:05:06 2020 +0200 @@ -46,7 +46,7 @@ version.project=Project version.name=Version version.status=Status -version.ordinal=Custom Ordering +version.ordinal=Ordering tooltip.ordinal=Use to override lexicographic ordering. placeholder.null-owner=Unassigned @@ -68,11 +68,8 @@ issue.subject=Subject issue.description=Description issue.assignee=Assignee -issue.affected-version=Affected Version issue.affected-versions=Affected Versions -issue.scheduled-version=Scheduled for Version issue.scheduled-versions=Scheduled for Versions -issue.resolved-version=Resolved in Version issue.resolved-versions=Resolved in Versions issue.category=Category issue.status=Status
--- a/src/main/resources/localization/projects_de.properties Sat May 30 15:28:27 2020 +0200 +++ b/src/main/resources/localization/projects_de.properties Sat May 30 18:05:06 2020 +0200 @@ -68,11 +68,8 @@ issue.subject=Thema issue.description=Beschreibung issue.assignee=Zugewiesen -issue.affected-version=Betroffene Version issue.affected-versions=Betroffene Versionen -issue.scheduled-version=Zielversion issue.scheduled-versions=Zielversionen -issue.resolved-version=Gel\u00f6st in Version issue.resolved-versions=Gel\u00f6st in Versionen issue.category=Kategorie issue.status=Status
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Sat May 30 15:28:27 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Sat May 30 18:05:06 2020 +0200 @@ -29,6 +29,7 @@ <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" /> +<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request" /> <jsp:useBean id="issue" type="de.uapcore.lightpit.entities.Issue" scope="request"/> <jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/> <jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/> @@ -44,13 +45,19 @@ <tr> <th><fmt:message key="issue.project"/></th> <td> + <c:if test="${issue.project.id ge 0}"> + <c:out value="${issue.project.name}" /> + <input type="hidden" name="pid" value="${issue.project.id}" /> + </c:if> + <c:if test="${empty issue.project or issue.project.id lt 0}"> <select name="pid" required> <c:forEach var="project" items="${projects}"> - <option value="${project.id}" <c:if test="${project eq issue.project}">selected</c:if> > + <option value="${project.id}"> <c:out value="${project.name}" /> </option> </c:forEach> </select> + </c:if> </td> </tr> <tr> @@ -104,45 +111,32 @@ </select> </td> </tr> + <c:if test="${issue.project.id ge 0}"> <tr> - <th> - <c:choose> - <c:when test="${issue.affectedVersions.size() gt 1}"> - <fmt:message key="issue.affected-versions"/> - </c:when> - <c:otherwise> - <fmt:message key="issue.affected-version"/> - </c:otherwise> - </c:choose> - </th> - <td>TODO</td> + <th class="vtop"><fmt:message key="issue.affected-versions"/></th> + <td> + <c:set var="fieldname" value="affected"/> + <c:set var="data" value="${issue.affectedVersions}" /> + <%@include file="../jspf/version-list.jsp"%> + </td> </tr> <tr> - <th> - <c:choose> - <c:when test="${issue.scheduledVersions.size() gt 1}"> - <fmt:message key="issue.scheduled-versions"/> - </c:when> - <c:otherwise> - <fmt:message key="issue.scheduled-version"/> - </c:otherwise> - </c:choose> - </th> - <td>TODO</td> + <th class="vtop"><fmt:message key="issue.scheduled-versions"/></th> + <td> + <c:set var="fieldname" value="scheduled"/> + <c:set var="data" value="${issue.scheduledVersions}" /> + <%@include file="../jspf/version-list.jsp"%> + </td> </tr> <tr> - <th> - <c:choose> - <c:when test="${issue.resolvedVersions.size() gt 1}"> - <fmt:message key="issue.resolved-versions"/> - </c:when> - <c:otherwise> - <fmt:message key="issue.resolved-version"/> - </c:otherwise> - </c:choose> - </th> - <td>TODO</td> + <th class="vtop"><fmt:message key="issue.resolved-versions"/></th> + <td> + <c:set var="fieldname" value="resolved"/> + <c:set var="data" value="${issue.resolvedVersions}" /> + <%@include file="../jspf/version-list.jsp"%> + </td> </tr> + </c:if> <tr> <th><fmt:message key="issue.eta"/></th> <td><input name="eta" type="date" value="<fmt:formatDate value="${issue.eta}" pattern="YYYY-MM-dd" />" /> </td>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp Sat May 30 15:28:27 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp Sat May 30 18:05:06 2020 +0200 @@ -47,13 +47,19 @@ <tr> <th><fmt:message key="version.project"/></th> <td> - <select name="pid" required> - <c:forEach var="project" items="${projects}"> - <option value="${project.id}" <c:if test="${project eq version.project}">selected</c:if> > - <c:out value="${project.name}" /> - </option> - </c:forEach> - </select> + <c:if test="${version.project.id ge 0}"> + <c:out value="${version.project.name}" /> + <input type="hidden" name="pid" value="${version.project.id}" /> + </c:if> + <c:if test="${empty version.project or version.project.id lt 0}"> + <select name="pid" required> + <c:forEach var="project" items="${projects}"> + <option value="${project.id}"> + <c:out value="${project.name}" /> + </option> + </c:forEach> + </select> + </c:if> </td> </tr> <tr>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/version-list.jsp Sat May 30 18:05:06 2020 +0200 @@ -0,0 +1,13 @@ +<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + +<select name="${fieldname}" multiple> + <c:forEach var="version" items="${versions}"> + <option value="${version.id}" + <c:forEach var="v" items="${data}"> + <c:if test="${v eq version}">selected</c:if> + </c:forEach> + > + <c:out value="${version.name}" /> + </option> + </c:forEach> +</select>