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