src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt

changeset 184
e8eecee6aadf
child 185
5ec9fcfbdf9c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Fri Apr 02 11:59:14 2021 +0200
@@ -0,0 +1,522 @@
+/*
+ * Copyright 2021 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package de.uapcore.lightpit.servlet
+
+import de.uapcore.lightpit.AbstractServlet
+import de.uapcore.lightpit.HttpRequest
+import de.uapcore.lightpit.dao.DataAccessObject
+import de.uapcore.lightpit.entities.*
+import de.uapcore.lightpit.types.IssueCategory
+import de.uapcore.lightpit.types.IssueStatus
+import de.uapcore.lightpit.types.VersionStatus
+import de.uapcore.lightpit.types.WebColor
+import de.uapcore.lightpit.util.AllFilter
+import de.uapcore.lightpit.util.IssueFilter
+import de.uapcore.lightpit.util.SpecificFilter
+import de.uapcore.lightpit.viewmodel.*
+import java.sql.Date
+import javax.servlet.annotation.WebServlet
+
+@WebServlet(urlPatterns = ["/projects/*"])
+class ProjectServlet : AbstractServlet() {
+
+    init {
+        get("/", this::projects)
+        get("/%project", this::project)
+        get("/%project/issues/%version/%component/", this::project)
+        get("/%project/edit", this::projectForm)
+        get("/-/create", this::projectForm)
+        post("/-/commit", this::projectCommit)
+
+        get("/%project/versions/", this::versions)
+        get("/%project/versions/%version/edit", this::versionForm)
+        get("/%project/versions/-/create", this::versionForm)
+        post("/%project/versions/-/commit", this::versionCommit)
+
+        get("/%project/components/", this::components)
+        get("/%project/components/%component/edit", this::componentForm)
+        get("/%project/components/-/create", this::componentForm)
+        post("/%project/components/-/commit", this::componentCommit)
+
+        get("/%project/issues/%version/%component/%issue", this::issue)
+        get("/%project/issues/%version/%component/%issue/edit", this::issueForm)
+        get("/%project/issues/%version/%component/%issue/comment", this::issueComment)
+        get("/%project/issues/%version/%component/-/create", this::issueForm)
+        get("/%project/issues/%version/%component/-/commit", this::issueCommit)
+    }
+
+    fun projects(http: HttpRequest, dao: DataAccessObject) {
+        val projects = dao.listProjects()
+        val projectInfos = projects.map {
+            ProjectInfo(
+                project = it,
+                versions = dao.listVersions(it),
+                components = emptyList(), // not required in this view
+                issueSummary = dao.collectIssueSummary(it)
+            )
+        }
+
+        with(http) {
+            view = ProjectsView(projectInfos)
+            navigationMenu = projectNavMenu(projects)
+            styleSheets = listOf("projects")
+            render("projects")
+        }
+    }
+
+    private fun activeProjectNavMenu(
+        projects: List<Project>,
+        projectInfo: ProjectInfo,
+        selectedVersion: Version? = null,
+        selectedComponent: Component? = null
+    ) =
+        projectNavMenu(
+            projects,
+            projectInfo.versions,
+            projectInfo.components,
+            projectInfo.project,
+            selectedVersion,
+            selectedComponent
+        )
+
+    sealed class LookupResult<T> {
+        class NotFound<T> : LookupResult<T>()
+        data class Found<T>(val elem: T?) : LookupResult<T>()
+    }
+
+    private fun <T : HasNode> HttpRequest.lookupPathParam(paramName: String, list: List<T>): LookupResult<T> {
+        val node = pathParams[paramName]
+        return if (node == null || node == "-") {
+            LookupResult.Found(null)
+        } else {
+            val result = list.find { it.node == node }
+            if (result == null) {
+                LookupResult.NotFound()
+            } else {
+                LookupResult.Found(result)
+            }
+        }
+    }
+
+    private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
+        val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null
+
+        val versions: List<Version> = dao.listVersions(project)
+        val components: List<Component> = dao.listComponents(project)
+
+        return ProjectInfo(
+            project,
+            versions,
+            components,
+            dao.collectIssueSummary(project)
+        )
+    }
+
+    private fun sanitizeNode(name: String): String {
+        val san = name.replace(Regex("[/\\\\]"), "-")
+        return if (san.startsWith(".")) {
+            "v$san"
+        } else {
+            san
+        }
+    }
+
+    data class PathInfos(
+        val projectInfo: ProjectInfo,
+        val version: Version?,
+        val component: Component?
+    ) {
+        val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/")
+    }
+
+    private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? {
+        val projectInfo = obtainProjectInfo(http, dao)
+        if (projectInfo == null) {
+            http.response.sendError(404)
+            return null
+        }
+
+        val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) {
+            is LookupResult.NotFound -> {
+                http.response.sendError(404)
+                return null
+            }
+            is LookupResult.Found -> {
+                result.elem
+            }
+        }
+        val component = when (val result = http.lookupPathParam("component", projectInfo.components)) {
+            is LookupResult.NotFound -> {
+                http.response.sendError(404)
+                return null
+            }
+            is LookupResult.Found -> {
+                result.elem
+            }
+        }
+
+        return PathInfos(projectInfo, version, component)
+    }
+
+    fun project(http: HttpRequest, dao: DataAccessObject) {
+        withPathInfo(http, dao)?.run {
+            val issues = dao.listIssues(IssueFilter(
+                project = SpecificFilter(projectInfo.project),
+                version = version?.let { SpecificFilter(it) } ?: AllFilter(),
+                component = component?.let { SpecificFilter(it) } ?: AllFilter()
+            ))
+
+            with(http) {
+                view = ProjectDetails(projectInfo, issues, version, component)
+                navigationMenu = activeProjectNavMenu(
+                    dao.listProjects(),
+                    projectInfo,
+                    version,
+                    component
+                )
+                styleSheets = listOf("projects")
+                render("project-details")
+            }
+        }
+    }
+
+    fun projectForm(http: HttpRequest, dao: DataAccessObject) {
+        val projectInfo = obtainProjectInfo(http, dao)
+        if (projectInfo == null) {
+            http.response.sendError(404)
+            return
+        }
+
+        with(http) {
+            view = ProjectEditView(projectInfo.project, dao.listUsers())
+            navigationMenu = activeProjectNavMenu(
+                dao.listProjects(),
+                projectInfo
+            )
+            styleSheets = listOf("projects")
+            render("project-form")
+        }
+    }
+
+    fun projectCommit(http: HttpRequest, dao: DataAccessObject) {
+        // TODO: replace defaults with throwing validator exceptions
+        val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply {
+            name = http.param("name") ?: ""
+            node = http.param("node") ?: ""
+            description = http.param("description") ?: ""
+            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
+            repoUrl = http.param("repoUrl") ?: ""
+            owner = (http.param("owner")?.toIntOrNull() ?: -1).let {
+                if (it < 0) null else dao.findUser(it)
+            }
+            // intentional defaults
+            if (node.isBlank()) node = name
+            // sanitizing
+            node = sanitizeNode(node)
+        }
+
+        if (project.id < 0) {
+            dao.insertProject(project)
+        } else {
+            dao.updateProject(project)
+        }
+
+        http.renderCommit("projects/${project.node}")
+    }
+
+    fun versions(http: HttpRequest, dao: DataAccessObject) {
+        val projectInfo = obtainProjectInfo(http, dao)
+        if (projectInfo == null) {
+            http.response.sendError(404)
+            return
+        }
+
+        with(http) {
+            view = VersionsView(
+                projectInfo,
+                dao.listVersionSummaries(projectInfo.project)
+            )
+            navigationMenu = activeProjectNavMenu(
+                dao.listProjects(),
+                projectInfo
+            )
+            styleSheets = listOf("projects")
+            render("versions")
+        }
+    }
+
+    fun versionForm(http: HttpRequest, dao: DataAccessObject) {
+        val projectInfo = obtainProjectInfo(http, dao)
+        if (projectInfo == null) {
+            http.response.sendError(404)
+            return
+        }
+
+        val version: Version
+        when (val result = http.lookupPathParam("version", projectInfo.versions)) {
+            is LookupResult.NotFound -> {
+                http.response.sendError(404)
+                return
+            }
+            is LookupResult.Found -> {
+                version = result.elem ?: Version(-1, projectInfo.project.id)
+            }
+        }
+
+        with(http) {
+            view = VersionEditView(projectInfo, version)
+            navigationMenu = activeProjectNavMenu(
+                dao.listProjects(),
+                projectInfo,
+                selectedVersion = version
+            )
+            styleSheets = listOf("projects")
+            render("version-form")
+        }
+    }
+
+    fun versionCommit(http: HttpRequest, dao: DataAccessObject) {
+        val id = http.param("id")?.toIntOrNull()
+        val projectid = http.param("projectid")?.toIntOrNull() ?: -1
+        val project = dao.findProject(projectid)
+        if (id == null || project == null) {
+            http.response.sendError(400)
+            return
+        }
+
+        // TODO: replace defaults with throwing validator exceptions
+        val version = Version(id, projectid).apply {
+            name = http.param("name") ?: ""
+            node = http.param("node") ?: ""
+            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
+            status = http.param("status")?.let(VersionStatus::valueOf) ?: VersionStatus.Future
+            // intentional defaults
+            if (node.isBlank()) node = name
+            // sanitizing
+            node = sanitizeNode(node)
+        }
+
+        if (id < 0) {
+            dao.insertVersion(version)
+        } else {
+            dao.updateVersion(version)
+        }
+
+        http.renderCommit("projects/${project.node}/versions/")
+    }
+
+    fun components(http: HttpRequest, dao: DataAccessObject) {
+        val projectInfo = obtainProjectInfo(http, dao)
+        if (projectInfo == null) {
+            http.response.sendError(404)
+            return
+        }
+
+        with(http) {
+            view = ComponentsView(
+                projectInfo,
+                dao.listComponentSummaries(projectInfo.project)
+            )
+            navigationMenu = activeProjectNavMenu(
+                dao.listProjects(),
+                projectInfo
+            )
+            styleSheets = listOf("projects")
+            render("components")
+        }
+    }
+
+    fun componentForm(http: HttpRequest, dao: DataAccessObject) {
+        val projectInfo = obtainProjectInfo(http, dao)
+        if (projectInfo == null) {
+            http.response.sendError(404)
+            return
+        }
+
+        val component: Component
+        when (val result = http.lookupPathParam("component", projectInfo.components)) {
+            is LookupResult.NotFound -> {
+                http.response.sendError(404)
+                return
+            }
+            is LookupResult.Found -> {
+                component = result.elem ?: Component(-1, projectInfo.project.id)
+            }
+        }
+
+        with(http) {
+            view = ComponentEditView(projectInfo, component, dao.listUsers())
+            navigationMenu = activeProjectNavMenu(
+                dao.listProjects(),
+                projectInfo,
+                selectedComponent = component
+            )
+            styleSheets = listOf("projects")
+            render("component-form")
+        }
+    }
+
+    fun componentCommit(http: HttpRequest, dao: DataAccessObject) {
+        val id = http.param("id")?.toIntOrNull()
+        val projectid = http.param("projectid")?.toIntOrNull() ?: -1
+        val project = dao.findProject(projectid)
+        if (id == null || project == null) {
+            http.response.sendError(400)
+            return
+        }
+
+        // TODO: replace defaults with throwing validator exceptions
+        val component = Component(id, projectid).apply {
+            name = http.param("name") ?: ""
+            node = http.param("node") ?: ""
+            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
+            color = WebColor(http.param("color") ?: "#000000")
+            description = http.param("description")
+            lead = (http.param("lead")?.toIntOrNull() ?: -1).let {
+                if (it < 0) null else dao.findUser(it)
+            }
+            // intentional defaults
+            if (node.isBlank()) node = name
+            // sanitizing
+            node = sanitizeNode(node)
+        }
+
+        if (id < 0) {
+            dao.insertComponent(component)
+        } else {
+            dao.updateComponent(component)
+        }
+
+        http.renderCommit("projects/${project.node}/components/")
+    }
+
+    fun issue(http: HttpRequest, dao: DataAccessObject) {
+        withPathInfo(http, dao)?.run {
+            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1)
+            if (issue == null) {
+                http.response.sendError(404)
+                return
+            }
+
+            val comments = dao.listComments(issue)
+
+            with(http) {
+                view = IssueDetailView(issue, comments, projectInfo.project, version, component)
+                navigationMenu = activeProjectNavMenu(
+                    dao.listProjects(),
+                    projectInfo,
+                    version,
+                    component
+                )
+                styleSheets = listOf("projects")
+                render("issue-view")
+            }
+        }
+    }
+
+    fun issueForm(http: HttpRequest, dao: DataAccessObject) {
+        withPathInfo(http, dao)?.run {
+            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue(
+                -1,
+                projectInfo.project,
+            )
+
+            // pre-select component, if available in the path info
+            issue.component = component
+
+            with(http) {
+                view = IssueEditView(
+                    issue,
+                    projectInfo.versions,
+                    projectInfo.components,
+                    dao.listUsers(),
+                    projectInfo.project,
+                    version,
+                    component
+                )
+                navigationMenu = activeProjectNavMenu(
+                    dao.listProjects(),
+                    projectInfo,
+                    version,
+                    component
+                )
+                styleSheets = listOf("projects")
+                render("issue-form")
+            }
+        }
+    }
+
+    fun issueComment(http: HttpRequest, dao: DataAccessObject) {
+        withPathInfo(http, dao)?.run {
+            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1)
+            if (issue == null) {
+                http.response.sendError(404)
+                return
+            }
+
+            // TODO: throw validator exception instead of using a default
+            val comment = IssueComment(-1, issue.id).apply {
+                author = http.remoteUser?.let { dao.findUserByName(it) }
+                comment = http.param("comment") ?: ""
+            }
+
+            dao.insertComment(comment)
+
+            http.renderCommit("${issuesHref}${issue.id}")
+        }
+    }
+
+    fun issueCommit(http: HttpRequest, dao: DataAccessObject) {
+        withPathInfo(http, dao)?.run {
+            // TODO: throw validator exception instead of using defaults
+            val issue = Issue(
+                http.param("id")?.toIntOrNull() ?: -1,
+                projectInfo.project
+            ).apply {
+                component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
+                category = IssueCategory.valueOf(http.param("category") ?: "")
+                status = IssueStatus.valueOf(http.param("status") ?: "")
+                subject = http.param("subject") ?: ""
+                description = http.param("description") ?: ""
+                assignee = http.param("assignee")?.toIntOrNull()?.let {
+                    when (it) {
+                        -1 -> null
+                        -2 -> component?.lead
+                        else -> dao.findUser(it)
+                    }
+                }
+                eta = http.param("eta")?.let { Date.valueOf(it) }
+
+                affectedVersions = http.paramArray("affected")
+                    .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } }
+                resolvedVersions = http.paramArray("resolved")
+                    .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } }
+            }
+
+            http.renderCommit("${issuesHref}${issue.id}")
+        }
+    }
+}
\ No newline at end of file

mercurial