Sun, 01 Aug 2021 17:01:59 +0200
#152 same sort order for versions and version summaries
/* * 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.IssueSorter.Companion.DEFAULT_ISSUE_SORTER 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) post("/%project/issues/%version/%component/%issue/comment", this::issueComment) get("/%project/issues/%version/%component/-/create", this::issueForm) post("/%project/issues/%version/%component/-/commit", this::issueCommit) } private 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 } } private fun feedPath(project: Project) = "feed/${project.node}/issues.rss" data class PathInfos( val projectInfo: ProjectInfo, val version: Version?, val component: Component? ) { val project = projectInfo.project val issuesHref by lazyOf("projects/${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) } private fun project(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.run { val issues = dao.listIssues(IssueFilter( project = SpecificFilter(project), version = version?.let { SpecificFilter(it) } ?: AllFilter(), component = component?.let { SpecificFilter(it) } ?: AllFilter() )).sortedWith(DEFAULT_ISSUE_SORTER) with(http) { view = ProjectDetails(projectInfo, issues, version, component) feedPath = feedPath(project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, version, component ) styleSheets = listOf("projects") render("project-details") } } } private fun projectForm(http: HttpRequest, dao: DataAccessObject) { if (!http.pathParams.containsKey("project")) { http.view = ProjectEditView(Project(-1), dao.listUsers()) http.navigationMenu = projectNavMenu(dao.listProjects()) } else { val projectInfo = obtainProjectInfo(http, dao) if (projectInfo == null) { http.response.sendError(404) return } http.view = ProjectEditView(projectInfo.project, dao.listUsers()) http.navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo ) } http.styleSheets = listOf("projects") http.render("project-form") } private 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}") } private 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) ) feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo ) styleSheets = listOf("projects") render("versions") } } private 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) feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, selectedVersion = version ) styleSheets = listOf("projects") render("version-form") } } private 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/") } private 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) ) feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo ) styleSheets = listOf("projects") render("components") } } private 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()) feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, selectedComponent = component ) styleSheets = listOf("projects") render("component-form") } } private 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/") } private 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, project, version, component) // TODO: feed path for this particular issue feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, version, component ) styleSheets = listOf("projects") render("issue-view") } } } private fun issueForm(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.run { val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue( -1, project, ) // pre-select component, if available in the path info issue.component = component // pre-select version, if available in the path info if (version != null) { if (version.status.isReleased) { issue.affectedVersions = listOf(version) } else { issue.resolvedVersions = listOf(version) } } with(http) { view = IssueEditView( issue, projectInfo.versions, projectInfo.components, dao.listUsers(), project, version, component ) feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, version, component ) styleSheets = listOf("projects") render("issue-form") } } } private 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}") } } private 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, 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 { if (it.isBlank()) null else Date.valueOf(it) } affectedVersions = http.paramArray("affected") .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, project.id) } } resolvedVersions = http.paramArray("resolved") .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, project.id) } } } val openId = if (issue.id < 0) { dao.insertIssue(issue) } else { dao.updateIssue(issue) issue.id } if (http.param("more") != null) { http.renderCommit("${issuesHref}-/create") } else { http.renderCommit("${issuesHref}${openId}") } } } }