adds version selection in issue editor

Sat, 30 May 2020 18:05:06 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 30 May 2020 18:05:06 +0200
changeset 83
24a3596b8f98
parent 82
4ec7f2600c83
child 84
5407d4ea0611

adds version selection in issue editor

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/version-list.jsp file | annotate | diff | comparison | revisions
--- 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>

mercurial