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

changeset 184
e8eecee6aadf
child 185
5ec9fcfbdf9c
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Fri Apr 02 11:59:14 2021 +0200
     1.3 @@ -0,0 +1,522 @@
     1.4 +/*
     1.5 + * Copyright 2021 Mike Becker. All rights reserved.
     1.6 + *
     1.7 + * Redistribution and use in source and binary forms, with or without
     1.8 + * modification, are permitted provided that the following conditions are met:
     1.9 + *
    1.10 + * 1. Redistributions of source code must retain the above copyright
    1.11 + * notice, this list of conditions and the following disclaimer.
    1.12 + *
    1.13 + * 2. Redistributions in binary form must reproduce the above copyright
    1.14 + * notice, this list of conditions and the following disclaimer in the
    1.15 + * documentation and/or other materials provided with the distribution.
    1.16 + *
    1.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    1.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    1.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    1.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    1.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    1.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    1.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    1.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    1.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    1.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    1.27 + */
    1.28 +
    1.29 +package de.uapcore.lightpit.servlet
    1.30 +
    1.31 +import de.uapcore.lightpit.AbstractServlet
    1.32 +import de.uapcore.lightpit.HttpRequest
    1.33 +import de.uapcore.lightpit.dao.DataAccessObject
    1.34 +import de.uapcore.lightpit.entities.*
    1.35 +import de.uapcore.lightpit.types.IssueCategory
    1.36 +import de.uapcore.lightpit.types.IssueStatus
    1.37 +import de.uapcore.lightpit.types.VersionStatus
    1.38 +import de.uapcore.lightpit.types.WebColor
    1.39 +import de.uapcore.lightpit.util.AllFilter
    1.40 +import de.uapcore.lightpit.util.IssueFilter
    1.41 +import de.uapcore.lightpit.util.SpecificFilter
    1.42 +import de.uapcore.lightpit.viewmodel.*
    1.43 +import java.sql.Date
    1.44 +import javax.servlet.annotation.WebServlet
    1.45 +
    1.46 +@WebServlet(urlPatterns = ["/projects/*"])
    1.47 +class ProjectServlet : AbstractServlet() {
    1.48 +
    1.49 +    init {
    1.50 +        get("/", this::projects)
    1.51 +        get("/%project", this::project)
    1.52 +        get("/%project/issues/%version/%component/", this::project)
    1.53 +        get("/%project/edit", this::projectForm)
    1.54 +        get("/-/create", this::projectForm)
    1.55 +        post("/-/commit", this::projectCommit)
    1.56 +
    1.57 +        get("/%project/versions/", this::versions)
    1.58 +        get("/%project/versions/%version/edit", this::versionForm)
    1.59 +        get("/%project/versions/-/create", this::versionForm)
    1.60 +        post("/%project/versions/-/commit", this::versionCommit)
    1.61 +
    1.62 +        get("/%project/components/", this::components)
    1.63 +        get("/%project/components/%component/edit", this::componentForm)
    1.64 +        get("/%project/components/-/create", this::componentForm)
    1.65 +        post("/%project/components/-/commit", this::componentCommit)
    1.66 +
    1.67 +        get("/%project/issues/%version/%component/%issue", this::issue)
    1.68 +        get("/%project/issues/%version/%component/%issue/edit", this::issueForm)
    1.69 +        get("/%project/issues/%version/%component/%issue/comment", this::issueComment)
    1.70 +        get("/%project/issues/%version/%component/-/create", this::issueForm)
    1.71 +        get("/%project/issues/%version/%component/-/commit", this::issueCommit)
    1.72 +    }
    1.73 +
    1.74 +    fun projects(http: HttpRequest, dao: DataAccessObject) {
    1.75 +        val projects = dao.listProjects()
    1.76 +        val projectInfos = projects.map {
    1.77 +            ProjectInfo(
    1.78 +                project = it,
    1.79 +                versions = dao.listVersions(it),
    1.80 +                components = emptyList(), // not required in this view
    1.81 +                issueSummary = dao.collectIssueSummary(it)
    1.82 +            )
    1.83 +        }
    1.84 +
    1.85 +        with(http) {
    1.86 +            view = ProjectsView(projectInfos)
    1.87 +            navigationMenu = projectNavMenu(projects)
    1.88 +            styleSheets = listOf("projects")
    1.89 +            render("projects")
    1.90 +        }
    1.91 +    }
    1.92 +
    1.93 +    private fun activeProjectNavMenu(
    1.94 +        projects: List<Project>,
    1.95 +        projectInfo: ProjectInfo,
    1.96 +        selectedVersion: Version? = null,
    1.97 +        selectedComponent: Component? = null
    1.98 +    ) =
    1.99 +        projectNavMenu(
   1.100 +            projects,
   1.101 +            projectInfo.versions,
   1.102 +            projectInfo.components,
   1.103 +            projectInfo.project,
   1.104 +            selectedVersion,
   1.105 +            selectedComponent
   1.106 +        )
   1.107 +
   1.108 +    sealed class LookupResult<T> {
   1.109 +        class NotFound<T> : LookupResult<T>()
   1.110 +        data class Found<T>(val elem: T?) : LookupResult<T>()
   1.111 +    }
   1.112 +
   1.113 +    private fun <T : HasNode> HttpRequest.lookupPathParam(paramName: String, list: List<T>): LookupResult<T> {
   1.114 +        val node = pathParams[paramName]
   1.115 +        return if (node == null || node == "-") {
   1.116 +            LookupResult.Found(null)
   1.117 +        } else {
   1.118 +            val result = list.find { it.node == node }
   1.119 +            if (result == null) {
   1.120 +                LookupResult.NotFound()
   1.121 +            } else {
   1.122 +                LookupResult.Found(result)
   1.123 +            }
   1.124 +        }
   1.125 +    }
   1.126 +
   1.127 +    private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
   1.128 +        val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null
   1.129 +
   1.130 +        val versions: List<Version> = dao.listVersions(project)
   1.131 +        val components: List<Component> = dao.listComponents(project)
   1.132 +
   1.133 +        return ProjectInfo(
   1.134 +            project,
   1.135 +            versions,
   1.136 +            components,
   1.137 +            dao.collectIssueSummary(project)
   1.138 +        )
   1.139 +    }
   1.140 +
   1.141 +    private fun sanitizeNode(name: String): String {
   1.142 +        val san = name.replace(Regex("[/\\\\]"), "-")
   1.143 +        return if (san.startsWith(".")) {
   1.144 +            "v$san"
   1.145 +        } else {
   1.146 +            san
   1.147 +        }
   1.148 +    }
   1.149 +
   1.150 +    data class PathInfos(
   1.151 +        val projectInfo: ProjectInfo,
   1.152 +        val version: Version?,
   1.153 +        val component: Component?
   1.154 +    ) {
   1.155 +        val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/")
   1.156 +    }
   1.157 +
   1.158 +    private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? {
   1.159 +        val projectInfo = obtainProjectInfo(http, dao)
   1.160 +        if (projectInfo == null) {
   1.161 +            http.response.sendError(404)
   1.162 +            return null
   1.163 +        }
   1.164 +
   1.165 +        val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) {
   1.166 +            is LookupResult.NotFound -> {
   1.167 +                http.response.sendError(404)
   1.168 +                return null
   1.169 +            }
   1.170 +            is LookupResult.Found -> {
   1.171 +                result.elem
   1.172 +            }
   1.173 +        }
   1.174 +        val component = when (val result = http.lookupPathParam("component", projectInfo.components)) {
   1.175 +            is LookupResult.NotFound -> {
   1.176 +                http.response.sendError(404)
   1.177 +                return null
   1.178 +            }
   1.179 +            is LookupResult.Found -> {
   1.180 +                result.elem
   1.181 +            }
   1.182 +        }
   1.183 +
   1.184 +        return PathInfos(projectInfo, version, component)
   1.185 +    }
   1.186 +
   1.187 +    fun project(http: HttpRequest, dao: DataAccessObject) {
   1.188 +        withPathInfo(http, dao)?.run {
   1.189 +            val issues = dao.listIssues(IssueFilter(
   1.190 +                project = SpecificFilter(projectInfo.project),
   1.191 +                version = version?.let { SpecificFilter(it) } ?: AllFilter(),
   1.192 +                component = component?.let { SpecificFilter(it) } ?: AllFilter()
   1.193 +            ))
   1.194 +
   1.195 +            with(http) {
   1.196 +                view = ProjectDetails(projectInfo, issues, version, component)
   1.197 +                navigationMenu = activeProjectNavMenu(
   1.198 +                    dao.listProjects(),
   1.199 +                    projectInfo,
   1.200 +                    version,
   1.201 +                    component
   1.202 +                )
   1.203 +                styleSheets = listOf("projects")
   1.204 +                render("project-details")
   1.205 +            }
   1.206 +        }
   1.207 +    }
   1.208 +
   1.209 +    fun projectForm(http: HttpRequest, dao: DataAccessObject) {
   1.210 +        val projectInfo = obtainProjectInfo(http, dao)
   1.211 +        if (projectInfo == null) {
   1.212 +            http.response.sendError(404)
   1.213 +            return
   1.214 +        }
   1.215 +
   1.216 +        with(http) {
   1.217 +            view = ProjectEditView(projectInfo.project, dao.listUsers())
   1.218 +            navigationMenu = activeProjectNavMenu(
   1.219 +                dao.listProjects(),
   1.220 +                projectInfo
   1.221 +            )
   1.222 +            styleSheets = listOf("projects")
   1.223 +            render("project-form")
   1.224 +        }
   1.225 +    }
   1.226 +
   1.227 +    fun projectCommit(http: HttpRequest, dao: DataAccessObject) {
   1.228 +        // TODO: replace defaults with throwing validator exceptions
   1.229 +        val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply {
   1.230 +            name = http.param("name") ?: ""
   1.231 +            node = http.param("node") ?: ""
   1.232 +            description = http.param("description") ?: ""
   1.233 +            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
   1.234 +            repoUrl = http.param("repoUrl") ?: ""
   1.235 +            owner = (http.param("owner")?.toIntOrNull() ?: -1).let {
   1.236 +                if (it < 0) null else dao.findUser(it)
   1.237 +            }
   1.238 +            // intentional defaults
   1.239 +            if (node.isBlank()) node = name
   1.240 +            // sanitizing
   1.241 +            node = sanitizeNode(node)
   1.242 +        }
   1.243 +
   1.244 +        if (project.id < 0) {
   1.245 +            dao.insertProject(project)
   1.246 +        } else {
   1.247 +            dao.updateProject(project)
   1.248 +        }
   1.249 +
   1.250 +        http.renderCommit("projects/${project.node}")
   1.251 +    }
   1.252 +
   1.253 +    fun versions(http: HttpRequest, dao: DataAccessObject) {
   1.254 +        val projectInfo = obtainProjectInfo(http, dao)
   1.255 +        if (projectInfo == null) {
   1.256 +            http.response.sendError(404)
   1.257 +            return
   1.258 +        }
   1.259 +
   1.260 +        with(http) {
   1.261 +            view = VersionsView(
   1.262 +                projectInfo,
   1.263 +                dao.listVersionSummaries(projectInfo.project)
   1.264 +            )
   1.265 +            navigationMenu = activeProjectNavMenu(
   1.266 +                dao.listProjects(),
   1.267 +                projectInfo
   1.268 +            )
   1.269 +            styleSheets = listOf("projects")
   1.270 +            render("versions")
   1.271 +        }
   1.272 +    }
   1.273 +
   1.274 +    fun versionForm(http: HttpRequest, dao: DataAccessObject) {
   1.275 +        val projectInfo = obtainProjectInfo(http, dao)
   1.276 +        if (projectInfo == null) {
   1.277 +            http.response.sendError(404)
   1.278 +            return
   1.279 +        }
   1.280 +
   1.281 +        val version: Version
   1.282 +        when (val result = http.lookupPathParam("version", projectInfo.versions)) {
   1.283 +            is LookupResult.NotFound -> {
   1.284 +                http.response.sendError(404)
   1.285 +                return
   1.286 +            }
   1.287 +            is LookupResult.Found -> {
   1.288 +                version = result.elem ?: Version(-1, projectInfo.project.id)
   1.289 +            }
   1.290 +        }
   1.291 +
   1.292 +        with(http) {
   1.293 +            view = VersionEditView(projectInfo, version)
   1.294 +            navigationMenu = activeProjectNavMenu(
   1.295 +                dao.listProjects(),
   1.296 +                projectInfo,
   1.297 +                selectedVersion = version
   1.298 +            )
   1.299 +            styleSheets = listOf("projects")
   1.300 +            render("version-form")
   1.301 +        }
   1.302 +    }
   1.303 +
   1.304 +    fun versionCommit(http: HttpRequest, dao: DataAccessObject) {
   1.305 +        val id = http.param("id")?.toIntOrNull()
   1.306 +        val projectid = http.param("projectid")?.toIntOrNull() ?: -1
   1.307 +        val project = dao.findProject(projectid)
   1.308 +        if (id == null || project == null) {
   1.309 +            http.response.sendError(400)
   1.310 +            return
   1.311 +        }
   1.312 +
   1.313 +        // TODO: replace defaults with throwing validator exceptions
   1.314 +        val version = Version(id, projectid).apply {
   1.315 +            name = http.param("name") ?: ""
   1.316 +            node = http.param("node") ?: ""
   1.317 +            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
   1.318 +            status = http.param("status")?.let(VersionStatus::valueOf) ?: VersionStatus.Future
   1.319 +            // intentional defaults
   1.320 +            if (node.isBlank()) node = name
   1.321 +            // sanitizing
   1.322 +            node = sanitizeNode(node)
   1.323 +        }
   1.324 +
   1.325 +        if (id < 0) {
   1.326 +            dao.insertVersion(version)
   1.327 +        } else {
   1.328 +            dao.updateVersion(version)
   1.329 +        }
   1.330 +
   1.331 +        http.renderCommit("projects/${project.node}/versions/")
   1.332 +    }
   1.333 +
   1.334 +    fun components(http: HttpRequest, dao: DataAccessObject) {
   1.335 +        val projectInfo = obtainProjectInfo(http, dao)
   1.336 +        if (projectInfo == null) {
   1.337 +            http.response.sendError(404)
   1.338 +            return
   1.339 +        }
   1.340 +
   1.341 +        with(http) {
   1.342 +            view = ComponentsView(
   1.343 +                projectInfo,
   1.344 +                dao.listComponentSummaries(projectInfo.project)
   1.345 +            )
   1.346 +            navigationMenu = activeProjectNavMenu(
   1.347 +                dao.listProjects(),
   1.348 +                projectInfo
   1.349 +            )
   1.350 +            styleSheets = listOf("projects")
   1.351 +            render("components")
   1.352 +        }
   1.353 +    }
   1.354 +
   1.355 +    fun componentForm(http: HttpRequest, dao: DataAccessObject) {
   1.356 +        val projectInfo = obtainProjectInfo(http, dao)
   1.357 +        if (projectInfo == null) {
   1.358 +            http.response.sendError(404)
   1.359 +            return
   1.360 +        }
   1.361 +
   1.362 +        val component: Component
   1.363 +        when (val result = http.lookupPathParam("component", projectInfo.components)) {
   1.364 +            is LookupResult.NotFound -> {
   1.365 +                http.response.sendError(404)
   1.366 +                return
   1.367 +            }
   1.368 +            is LookupResult.Found -> {
   1.369 +                component = result.elem ?: Component(-1, projectInfo.project.id)
   1.370 +            }
   1.371 +        }
   1.372 +
   1.373 +        with(http) {
   1.374 +            view = ComponentEditView(projectInfo, component, dao.listUsers())
   1.375 +            navigationMenu = activeProjectNavMenu(
   1.376 +                dao.listProjects(),
   1.377 +                projectInfo,
   1.378 +                selectedComponent = component
   1.379 +            )
   1.380 +            styleSheets = listOf("projects")
   1.381 +            render("component-form")
   1.382 +        }
   1.383 +    }
   1.384 +
   1.385 +    fun componentCommit(http: HttpRequest, dao: DataAccessObject) {
   1.386 +        val id = http.param("id")?.toIntOrNull()
   1.387 +        val projectid = http.param("projectid")?.toIntOrNull() ?: -1
   1.388 +        val project = dao.findProject(projectid)
   1.389 +        if (id == null || project == null) {
   1.390 +            http.response.sendError(400)
   1.391 +            return
   1.392 +        }
   1.393 +
   1.394 +        // TODO: replace defaults with throwing validator exceptions
   1.395 +        val component = Component(id, projectid).apply {
   1.396 +            name = http.param("name") ?: ""
   1.397 +            node = http.param("node") ?: ""
   1.398 +            ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
   1.399 +            color = WebColor(http.param("color") ?: "#000000")
   1.400 +            description = http.param("description")
   1.401 +            lead = (http.param("lead")?.toIntOrNull() ?: -1).let {
   1.402 +                if (it < 0) null else dao.findUser(it)
   1.403 +            }
   1.404 +            // intentional defaults
   1.405 +            if (node.isBlank()) node = name
   1.406 +            // sanitizing
   1.407 +            node = sanitizeNode(node)
   1.408 +        }
   1.409 +
   1.410 +        if (id < 0) {
   1.411 +            dao.insertComponent(component)
   1.412 +        } else {
   1.413 +            dao.updateComponent(component)
   1.414 +        }
   1.415 +
   1.416 +        http.renderCommit("projects/${project.node}/components/")
   1.417 +    }
   1.418 +
   1.419 +    fun issue(http: HttpRequest, dao: DataAccessObject) {
   1.420 +        withPathInfo(http, dao)?.run {
   1.421 +            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1)
   1.422 +            if (issue == null) {
   1.423 +                http.response.sendError(404)
   1.424 +                return
   1.425 +            }
   1.426 +
   1.427 +            val comments = dao.listComments(issue)
   1.428 +
   1.429 +            with(http) {
   1.430 +                view = IssueDetailView(issue, comments, projectInfo.project, version, component)
   1.431 +                navigationMenu = activeProjectNavMenu(
   1.432 +                    dao.listProjects(),
   1.433 +                    projectInfo,
   1.434 +                    version,
   1.435 +                    component
   1.436 +                )
   1.437 +                styleSheets = listOf("projects")
   1.438 +                render("issue-view")
   1.439 +            }
   1.440 +        }
   1.441 +    }
   1.442 +
   1.443 +    fun issueForm(http: HttpRequest, dao: DataAccessObject) {
   1.444 +        withPathInfo(http, dao)?.run {
   1.445 +            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue(
   1.446 +                -1,
   1.447 +                projectInfo.project,
   1.448 +            )
   1.449 +
   1.450 +            // pre-select component, if available in the path info
   1.451 +            issue.component = component
   1.452 +
   1.453 +            with(http) {
   1.454 +                view = IssueEditView(
   1.455 +                    issue,
   1.456 +                    projectInfo.versions,
   1.457 +                    projectInfo.components,
   1.458 +                    dao.listUsers(),
   1.459 +                    projectInfo.project,
   1.460 +                    version,
   1.461 +                    component
   1.462 +                )
   1.463 +                navigationMenu = activeProjectNavMenu(
   1.464 +                    dao.listProjects(),
   1.465 +                    projectInfo,
   1.466 +                    version,
   1.467 +                    component
   1.468 +                )
   1.469 +                styleSheets = listOf("projects")
   1.470 +                render("issue-form")
   1.471 +            }
   1.472 +        }
   1.473 +    }
   1.474 +
   1.475 +    fun issueComment(http: HttpRequest, dao: DataAccessObject) {
   1.476 +        withPathInfo(http, dao)?.run {
   1.477 +            val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1)
   1.478 +            if (issue == null) {
   1.479 +                http.response.sendError(404)
   1.480 +                return
   1.481 +            }
   1.482 +
   1.483 +            // TODO: throw validator exception instead of using a default
   1.484 +            val comment = IssueComment(-1, issue.id).apply {
   1.485 +                author = http.remoteUser?.let { dao.findUserByName(it) }
   1.486 +                comment = http.param("comment") ?: ""
   1.487 +            }
   1.488 +
   1.489 +            dao.insertComment(comment)
   1.490 +
   1.491 +            http.renderCommit("${issuesHref}${issue.id}")
   1.492 +        }
   1.493 +    }
   1.494 +
   1.495 +    fun issueCommit(http: HttpRequest, dao: DataAccessObject) {
   1.496 +        withPathInfo(http, dao)?.run {
   1.497 +            // TODO: throw validator exception instead of using defaults
   1.498 +            val issue = Issue(
   1.499 +                http.param("id")?.toIntOrNull() ?: -1,
   1.500 +                projectInfo.project
   1.501 +            ).apply {
   1.502 +                component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
   1.503 +                category = IssueCategory.valueOf(http.param("category") ?: "")
   1.504 +                status = IssueStatus.valueOf(http.param("status") ?: "")
   1.505 +                subject = http.param("subject") ?: ""
   1.506 +                description = http.param("description") ?: ""
   1.507 +                assignee = http.param("assignee")?.toIntOrNull()?.let {
   1.508 +                    when (it) {
   1.509 +                        -1 -> null
   1.510 +                        -2 -> component?.lead
   1.511 +                        else -> dao.findUser(it)
   1.512 +                    }
   1.513 +                }
   1.514 +                eta = http.param("eta")?.let { Date.valueOf(it) }
   1.515 +
   1.516 +                affectedVersions = http.paramArray("affected")
   1.517 +                    .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } }
   1.518 +                resolvedVersions = http.paramArray("resolved")
   1.519 +                    .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } }
   1.520 +            }
   1.521 +
   1.522 +            http.renderCommit("${issuesHref}${issue.id}")
   1.523 +        }
   1.524 +    }
   1.525 +}
   1.526 \ No newline at end of file

mercurial