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

Sat, 22 Jul 2023 22:32:04 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 22 Jul 2023 22:32:04 +0200
changeset 284
671c1c8fbf1c
parent 271
f8f5e82944fa
child 292
703591e739f4
permissions
-rw-r--r--

add full support for commit references - fixes #276

     1 /*
     2  * Copyright 2021 Mike Becker. All rights reserved.
     3  *
     4  * Redistribution and use in source and binary forms, with or without
     5  * modification, are permitted provided that the following conditions are met:
     6  *
     7  * 1. Redistributions of source code must retain the above copyright
     8  * notice, this list of conditions and the following disclaimer.
     9  *
    10  * 2. Redistributions in binary form must reproduce the above copyright
    11  * notice, this list of conditions and the following disclaimer in the
    12  * documentation and/or other materials provided with the distribution.
    13  *
    14  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    17  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    20  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    21  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    22  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    24  */
    26 package de.uapcore.lightpit.servlet
    28 import de.uapcore.lightpit.AbstractServlet
    29 import de.uapcore.lightpit.HttpRequest
    30 import de.uapcore.lightpit.boolValidator
    31 import de.uapcore.lightpit.dao.DataAccessObject
    32 import de.uapcore.lightpit.dateOptValidator
    33 import de.uapcore.lightpit.entities.*
    34 import de.uapcore.lightpit.types.*
    35 import de.uapcore.lightpit.viewmodel.*
    36 import jakarta.servlet.annotation.WebServlet
    37 import java.sql.Date
    39 @WebServlet(urlPatterns = ["/projects/*"])
    40 class ProjectServlet : AbstractServlet() {
    42     init {
    43         get("/", this::projects)
    44         get("/%project", this::project)
    45         get("/%project/issues/%version/%component/", this::project)
    46         get("/%project/edit", this::projectForm)
    47         get("/-/create", this::projectForm)
    48         post("/-/commit", this::projectCommit)
    49         post("/%project/vcs/analyze", this::vcsAnalyze)
    51         get("/%project/versions/", this::versions)
    52         get("/%project/versions/%version/edit", this::versionForm)
    53         get("/%project/versions/-/create", this::versionForm)
    54         post("/%project/versions/-/commit", this::versionCommit)
    56         get("/%project/components/", this::components)
    57         get("/%project/components/%component/edit", this::componentForm)
    58         get("/%project/components/-/create", this::componentForm)
    59         post("/%project/components/-/commit", this::componentCommit)
    61         get("/%project/issues/%version/%component/%issue", this::issue)
    62         get("/%project/issues/%version/%component/%issue/edit", this::issueForm)
    63         post("/%project/issues/%version/%component/%issue/comment", this::issueComment)
    64         post("/%project/issues/%version/%component/%issue/relation", this::issueRelation)
    65         get("/%project/issues/%version/%component/%issue/removeRelation", this::issueRemoveRelation)
    66         get("/%project/issues/%version/%component/-/create", this::issueForm)
    67         post("/%project/issues/%version/%component/-/commit", this::issueCommit)
    68     }
    70     private fun projects(http: HttpRequest, dao: DataAccessObject) {
    71         val projects = dao.listProjects()
    72         val projectInfos = projects.map {
    73             ProjectInfo(
    74                 project = it,
    75                 versions = dao.listVersions(it),
    76                 components = emptyList(), // not required in this view
    77                 issueSummary = dao.collectIssueSummary(it)
    78             )
    79         }
    81         with(http) {
    82             view = ProjectsView(projectInfos)
    83             navigationMenu = projectNavMenu(projects)
    84             styleSheets = listOf("projects")
    85             render("projects")
    86         }
    87     }
    89     private fun activeProjectNavMenu(
    90         projects: List<Project>,
    91         projectInfo: ProjectInfo,
    92         selectedVersion: Version? = null,
    93         selectedComponent: Component? = null
    94     ) =
    95         projectNavMenu(
    96             projects,
    97             projectInfo.versions,
    98             projectInfo.components,
    99             projectInfo.project,
   100             selectedVersion,
   101             selectedComponent
   102         )
   104     private sealed interface LookupResult<T>
   105     private class NotFound<T> : LookupResult<T>
   106     private data class Found<T>(val elem: T?) : LookupResult<T>
   108     private fun <T : HasNode> HttpRequest.lookupPathParam(paramName: String, list: List<T>): LookupResult<T> {
   109         val node = pathParams[paramName]
   110         return if (node == null || node == "-") {
   111             Found(null)
   112         } else {
   113             val result = list.find { it.node == node }
   114             if (result == null) {
   115                 NotFound()
   116             } else {
   117                 Found(result)
   118             }
   119         }
   120     }
   122     private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
   123         val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null
   125         val versions: List<Version> = dao.listVersions(project)
   126         val components: List<Component> = dao.listComponents(project)
   128         return ProjectInfo(
   129             project,
   130             versions,
   131             components,
   132             dao.collectIssueSummary(project)
   133         )
   134     }
   136     private fun sanitizeNode(name: String): String {
   137         val san = name.replace(Regex("[/\\\\]"), "-")
   138         return if (san.startsWith(".")) {
   139             "v$san"
   140         } else {
   141             san
   142         }
   143     }
   145     private fun feedPath(project: Project) = "feed/${project.node}/issues.rss"
   147     private data class PathInfos(
   148         val projectInfo: ProjectInfo,
   149         val version: Version?,
   150         val component: Component?
   151     ) {
   152         val project = projectInfo.project
   153         val issuesHref by lazyOf("projects/${project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/")
   154     }
   156     private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? {
   157         val projectInfo = obtainProjectInfo(http, dao)
   158         if (projectInfo == null) {
   159             http.response.sendError(404)
   160             return null
   161         }
   163         val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) {
   164             is NotFound -> {
   165                 http.response.sendError(404)
   166                 return null
   167             }
   168             is Found -> {
   169                 result.elem
   170             }
   171         }
   172         val component = when (val result = http.lookupPathParam("component", projectInfo.components)) {
   173             is NotFound -> {
   174                 http.response.sendError(404)
   175                 return null
   176             }
   177             is Found -> {
   178                 result.elem
   179             }
   180         }
   182         return PathInfos(projectInfo, version, component)
   183     }
   185     private fun project(http: HttpRequest, dao: DataAccessObject) {
   186         withPathInfo(http, dao)?.run {
   188             val filter = IssueFilter(http)
   190             val needRelationsMap = filter.onlyBlocker
   192             val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap()
   194             val issues = dao.listIssues(project, filter.includeDone, version, component)
   195                 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary))
   196                 .filter {
   197                     (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) &&
   198                     (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) &&
   199                     (filter.status.isEmpty() || filter.status.contains(it.status)) &&
   200                     (filter.category.isEmpty() || filter.category.contains(it.category))
   201                 }
   203             with(http) {
   204                 pageTitle = project.name
   205                 view = ProjectDetails(projectInfo, issues, filter, version, component)
   206                 feedPath = feedPath(project)
   207                 navigationMenu = activeProjectNavMenu(
   208                     dao.listProjects(),
   209                     projectInfo,
   210                     version,
   211                     component
   212                 )
   213                 styleSheets = listOf("projects")
   214                 javascript = "project-details"
   215                 render("project-details")
   216             }
   217         }
   218     }
   220     private fun projectForm(http: HttpRequest, dao: DataAccessObject) {
   221         if (!http.pathParams.containsKey("project")) {
   222             http.view = ProjectEditView(Project(-1), dao.listUsers())
   223             http.navigationMenu = projectNavMenu(dao.listProjects())
   224         } else {
   225             val projectInfo = obtainProjectInfo(http, dao)
   226             if (projectInfo == null) {
   227                 http.response.sendError(404)
   228                 return
   229             }
   230             http.view = ProjectEditView(projectInfo.project, dao.listUsers())
   231             http.navigationMenu = activeProjectNavMenu(
   232                 dao.listProjects(),
   233                 projectInfo
   234             )
   235         }
   236         http.styleSheets = listOf("projects")
   237         http.render("project-form")
   238     }
   240     private fun projectCommit(http: HttpRequest, dao: DataAccessObject) {
   241         val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply {
   242             name = http.param("name") ?: ""
   243             node = http.param("node") ?: ""
   244             description = http.param("description") ?: ""
   245             ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
   246             repoUrl = http.param("repoUrl") ?: ""
   247             vcs = VcsType.valueOf(http.param("vcs") ?: "None")
   248             owner = (http.param("owner")?.toIntOrNull() ?: -1).let {
   249                 if (it < 0) null else dao.findUser(it)
   250             }
   251             // intentional defaults
   252             if (node.isBlank()) node = name
   253             // sanitizing
   254             node = sanitizeNode(node)
   255         }
   257         if (project.id < 0) {
   258             dao.insertProject(project)
   259         } else {
   260             dao.updateProject(project)
   261         }
   263         http.renderCommit("projects/${project.node}")
   264     }
   266     private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) {
   267         val projectInfo = obtainProjectInfo(http, dao)
   268         if (projectInfo == null) {
   269             http.response.sendError(404)
   270             return
   271         }
   273         // if analysis is not configured, reject the request
   274         if (projectInfo.project.vcs == VcsType.None) {
   275             http.response.sendError(404)
   276             return
   277         }
   279         // obtain the list of issues for this project to filter cross-project references
   280         val knownIds = dao.listIssues(projectInfo.project, true).map { it.id }
   282         // read the provided commit log and merge only the refs that relate issues from the current project
   283         dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) })
   284     }
   286     private fun versions(http: HttpRequest, dao: DataAccessObject) {
   287         val projectInfo = obtainProjectInfo(http, dao)
   288         if (projectInfo == null) {
   289             http.response.sendError(404)
   290             return
   291         }
   293         with(http) {
   294             pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.versions")}"
   295             view = VersionsView(
   296                 projectInfo,
   297                 dao.listVersionSummaries(projectInfo.project)
   298             )
   299             feedPath = feedPath(projectInfo.project)
   300             navigationMenu = activeProjectNavMenu(
   301                 dao.listProjects(),
   302                 projectInfo
   303             )
   304             styleSheets = listOf("projects")
   305             javascript = "project-details"
   306             render("versions")
   307         }
   308     }
   310     private fun versionForm(http: HttpRequest, dao: DataAccessObject) {
   311         val projectInfo = obtainProjectInfo(http, dao)
   312         if (projectInfo == null) {
   313             http.response.sendError(404)
   314             return
   315         }
   317         val version: Version
   318         when (val result = http.lookupPathParam("version", projectInfo.versions)) {
   319             is NotFound -> {
   320                 http.response.sendError(404)
   321                 return
   322             }
   323             is Found -> {
   324                 version = result.elem ?: Version(-1, projectInfo.project.id)
   325             }
   326         }
   328         with(http) {
   329             view = VersionEditView(projectInfo, version)
   330             feedPath = feedPath(projectInfo.project)
   331             navigationMenu = activeProjectNavMenu(
   332                 dao.listProjects(),
   333                 projectInfo,
   334                 selectedVersion = version
   335             )
   336             styleSheets = listOf("projects")
   337             render("version-form")
   338         }
   339     }
   341     private fun obtainIdAndProject(http: HttpRequest, dao: DataAccessObject): Pair<Int, Project>? {
   342         val id = http.param("id")?.toIntOrNull()
   343         val projectid = http.param("projectid")?.toIntOrNull() ?: -1
   344         val project = dao.findProject(projectid)
   345         return if (id == null || project == null) {
   346             http.response.sendError(400)
   347             null
   348         } else {
   349             Pair(id, project)
   350         }
   351     }
   353     private fun versionCommit(http: HttpRequest, dao: DataAccessObject) {
   354         val idParams = obtainIdAndProject(http, dao) ?: return
   355         val (id, project) = idParams
   357         val version = Version(id, project.id).apply {
   358             name = http.param("name") ?: ""
   359             node = http.param("node") ?: ""
   360             ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
   361             status = http.param("status")?.let(VersionStatus::valueOf) ?: VersionStatus.Future
   362             // TODO: process error messages
   363             eol = http.param("eol", ::dateOptValidator, null, mutableListOf())
   364             release = http.param("release", ::dateOptValidator, null, mutableListOf())
   365             // intentional defaults
   366             if (node.isBlank()) node = name
   367             // sanitizing
   368             node = sanitizeNode(node)
   369         }
   371         // sanitize eol and release date
   372         if (version.status.isEndOfLife) {
   373             if (version.eol == null) version.eol = Date(System.currentTimeMillis())
   374         } else if (version.status.isReleased) {
   375             if (version.release == null) version.release = Date(System.currentTimeMillis())
   376         }
   378         if (id < 0) {
   379             dao.insertVersion(version)
   380         } else {
   381             dao.updateVersion(version)
   382         }
   384         http.renderCommit("projects/${project.node}/versions/")
   385     }
   387     private fun components(http: HttpRequest, dao: DataAccessObject) {
   388         val projectInfo = obtainProjectInfo(http, dao)
   389         if (projectInfo == null) {
   390             http.response.sendError(404)
   391             return
   392         }
   394         with(http) {
   395             pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.components")}"
   396             view = ComponentsView(
   397                 projectInfo,
   398                 dao.listComponentSummaries(projectInfo.project)
   399             )
   400             feedPath = feedPath(projectInfo.project)
   401             navigationMenu = activeProjectNavMenu(
   402                 dao.listProjects(),
   403                 projectInfo
   404             )
   405             styleSheets = listOf("projects")
   406             javascript = "project-details"
   407             render("components")
   408         }
   409     }
   411     private fun componentForm(http: HttpRequest, dao: DataAccessObject) {
   412         val projectInfo = obtainProjectInfo(http, dao)
   413         if (projectInfo == null) {
   414             http.response.sendError(404)
   415             return
   416         }
   418         val component: Component
   419         when (val result = http.lookupPathParam("component", projectInfo.components)) {
   420             is NotFound -> {
   421                 http.response.sendError(404)
   422                 return
   423             }
   424             is Found -> {
   425                 component = result.elem ?: Component(-1, projectInfo.project.id)
   426             }
   427         }
   429         with(http) {
   430             view = ComponentEditView(projectInfo, component, dao.listUsers())
   431             feedPath = feedPath(projectInfo.project)
   432             navigationMenu = activeProjectNavMenu(
   433                 dao.listProjects(),
   434                 projectInfo,
   435                 selectedComponent = component
   436             )
   437             styleSheets = listOf("projects")
   438             render("component-form")
   439         }
   440     }
   442     private fun componentCommit(http: HttpRequest, dao: DataAccessObject) {
   443         val idParams = obtainIdAndProject(http, dao) ?: return
   444         val (id, project) = idParams
   446         val component = Component(id, project.id).apply {
   447             name = http.param("name") ?: ""
   448             node = http.param("node") ?: ""
   449             ordinal = http.param("ordinal")?.toIntOrNull() ?: 0
   450             color = WebColor(http.param("color") ?: "#000000")
   451             description = http.param("description")
   452             // TODO: process error message
   453             active = http.param("active", ::boolValidator, true, mutableListOf())
   454             lead = (http.param("lead")?.toIntOrNull() ?: -1).let {
   455                 if (it < 0) null else dao.findUser(it)
   456             }
   457             // intentional defaults
   458             if (node.isBlank()) node = name
   459             // sanitizing
   460             node = sanitizeNode(node)
   461         }
   463         if (id < 0) {
   464             dao.insertComponent(component)
   465         } else {
   466             dao.updateComponent(component)
   467         }
   469         http.renderCommit("projects/${project.node}/components/")
   470     }
   472     private fun issue(http: HttpRequest, dao: DataAccessObject) {
   473         val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue)
   474         if (issue == null) {
   475             http.response.sendError(404)
   476             return
   477         }
   478         renderIssueView(http, dao, issue)
   479     }
   481     private fun renderIssueView(
   482         http: HttpRequest,
   483         dao: DataAccessObject,
   484         issue: Issue,
   485         relationError: String? = null
   486     ) {
   487         withPathInfo(http, dao)?.run {
   488             val comments = dao.listComments(issue)
   490             with(http) {
   491                 pageTitle = "${projectInfo.project.name}: #${issue.id} ${issue.subject}"
   492                 view = IssueDetailView(
   493                     issue,
   494                     comments,
   495                     project,
   496                     version,
   497                     component,
   498                     dao.listIssues(project, true),
   499                     dao.listIssueRelations(issue),
   500                     relationError,
   501                     dao.listCommitRefs(issue)
   502                 )
   503                 feedPath = feedPath(projectInfo.project)
   504                 navigationMenu = activeProjectNavMenu(
   505                     dao.listProjects(),
   506                     projectInfo,
   507                     version,
   508                     component
   509                 )
   510                 styleSheets = listOf("projects")
   511                 javascript = "issue-editor"
   512                 render("issue-view")
   513             }
   514         }
   515     }
   517     private fun issueForm(http: HttpRequest, dao: DataAccessObject) {
   518         withPathInfo(http, dao)?.run {
   519             val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue(
   520                 -1,
   521                 project,
   522             )
   524             // for new issues set some defaults
   525             if (issue.id < 0) {
   526                 // pre-select component, if available in the path info
   527                 issue.component = component
   529                 // pre-select version, if available in the path info
   530                 if (version != null) {
   531                     if (version.status.isReleased) {
   532                         issue.affected = version
   533                     } else {
   534                         issue.resolved = version
   535                     }
   536                 }
   537             }
   539             with(http) {
   540                 view = IssueEditView(
   541                     issue,
   542                     projectInfo.versions,
   543                     projectInfo.components,
   544                     dao.listUsers(),
   545                     project,
   546                     version,
   547                     component
   548                 )
   549                 feedPath = feedPath(projectInfo.project)
   550                 navigationMenu = activeProjectNavMenu(
   551                     dao.listProjects(),
   552                     projectInfo,
   553                     version,
   554                     component
   555                 )
   556                 styleSheets = listOf("projects")
   557                 javascript = "issue-editor"
   558                 render("issue-form")
   559             }
   560         }
   561     }
   563     private fun issueComment(http: HttpRequest, dao: DataAccessObject) {
   564         withPathInfo(http, dao)?.run {
   565             val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue)
   566             if (issue == null) {
   567                 http.response.sendError(404)
   568                 return
   569             }
   571             val commentId = http.param("commentid")?.toIntOrNull() ?: -1
   572             if (commentId > 0) {
   573                 val comment = dao.findComment(commentId)
   574                 if (comment == null) {
   575                     http.response.sendError(404)
   576                     return
   577                 }
   578                 val originalAuthor = comment.author?.username
   579                 if (originalAuthor != null && originalAuthor == http.remoteUser) {
   580                     val newComment = http.param("comment")
   581                     if (!newComment.isNullOrBlank()) {
   582                         comment.comment = newComment
   583                         dao.updateComment(comment)
   584                         dao.insertHistoryEvent(issue, comment)
   585                     } else {
   586                         logger.debug("Not updating comment ${comment.id} because nothing changed.")
   587                     }
   588                 } else {
   589                     http.response.sendError(403)
   590                     return
   591                 }
   592             } else {
   593                 val comment = IssueComment(-1, issue.id).apply {
   594                     author = http.remoteUser?.let { dao.findUserByName(it) }
   595                     comment = http.param("comment") ?: ""
   596                 }
   597                 val newId = dao.insertComment(comment)
   598                 dao.insertHistoryEvent(issue, comment, newId)
   599             }
   601             http.renderCommit("${issuesHref}${issue.id}")
   602         }
   603     }
   605     private fun issueCommit(http: HttpRequest, dao: DataAccessObject) {
   606         withPathInfo(http, dao)?.run {
   607             val issue = Issue(
   608                 http.param("id")?.toIntOrNull() ?: -1,
   609                 project
   610             ).apply {
   611                 component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
   612                 category = IssueCategory.valueOf(http.param("category") ?: "")
   613                 status = IssueStatus.valueOf(http.param("status") ?: "")
   614                 subject = http.param("subject") ?: ""
   615                 description = http.param("description") ?: ""
   616                 assignee = http.param("assignee")?.toIntOrNull()?.let {
   617                     when (it) {
   618                         -1 -> null
   619                         -2 -> component?.lead
   620                         else -> dao.findUser(it)
   621                     }
   622                 }
   623                 // TODO: process error messages
   624                 eta = http.param("eta", ::dateOptValidator, null, mutableListOf())
   626                 affected = http.param("affected")?.toIntOrNull()?.takeIf { it > 0 }?.let { Version(it, project.id) }
   627                 resolved = http.param("resolved")?.toIntOrNull()?.takeIf { it > 0 }?.let { Version(it, project.id) }
   628             }
   630             val openId = if (issue.id < 0) {
   631                 val id = dao.insertIssue(issue)
   632                 dao.insertHistoryEvent(issue, id)
   633                 id
   634             } else {
   635                 val reference = dao.findIssue(issue.id)
   636                 if (reference == null) {
   637                     http.response.sendError(404)
   638                     return
   639                 }
   641                 if (issue.hasChanged(reference)) {
   642                     dao.updateIssue(issue)
   643                     dao.insertHistoryEvent(issue)
   644                 } else {
   645                     logger.debug("Not updating issue ${issue.id} because nothing changed.")
   646                 }
   648                 val newComment = http.param("comment")
   649                 if (!newComment.isNullOrBlank()) {
   650                     val comment = IssueComment(-1, issue.id).apply {
   651                         author = http.remoteUser?.let { dao.findUserByName(it) }
   652                         comment = newComment
   653                     }
   654                     val commentid = dao.insertComment(comment)
   655                     dao.insertHistoryEvent(issue, comment, commentid)
   656                 }
   657                 issue.id
   658             }
   660             if (http.param("more") != null) {
   661                 http.renderCommit("${issuesHref}-/create")
   662             } else {
   663                 http.renderCommit("${issuesHref}${openId}")
   664             }
   665         }
   666     }
   668     private fun issueRelation(http: HttpRequest, dao: DataAccessObject) {
   669         withPathInfo(http, dao)?.run {
   670             val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue)
   671             if (issue == null) {
   672                 http.response.sendError(404)
   673                 return
   674             }
   676             // determine the relation type
   677             val type: Pair<RelationType, Boolean>? = http.param("type")?.let {
   678                 try {
   679                     if (it.startsWith("!")) {
   680                         Pair(RelationType.valueOf(it.substring(1)), true)
   681                     } else {
   682                         Pair(RelationType.valueOf(it), false)
   683                     }
   684                 } catch (_: IllegalArgumentException) {
   685                     null
   686                 }
   687             }
   689             // if the relation type was invalid, send HTTP 500
   690             if (type == null) {
   691                 http.response.sendError(500)
   692                 return
   693             }
   695             // determine the target issue
   696             val targetIssue: Issue? = http.param("issue")?.let {
   697                 if (it.startsWith("#") && it.length > 1) {
   698                     it.substring(1).split(" ", limit = 2)[0].toIntOrNull()
   699                         ?.let(dao::findIssue)
   700                         ?.takeIf { target -> target.project.id == issue.project.id }
   701                 } else {
   702                     null
   703                 }
   704             }
   706             // check if the target issue is valid
   707             if (targetIssue == null) {
   708                 renderIssueView(http, dao, issue, "issue.relations.target.invalid")
   709                 return
   710             }
   712             // commit the result
   713             dao.insertIssueRelation(IssueRelation(issue, targetIssue, type.first, type.second))
   714             http.renderCommit("${issuesHref}${issue.id}")
   715         }
   716     }
   718     private fun issueRemoveRelation(http: HttpRequest, dao: DataAccessObject) {
   719         withPathInfo(http, dao)?.run {
   720             val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue)
   721             if (issue == null) {
   722                 http.response.sendError(404)
   723                 return
   724             }
   726             // determine relation
   727             val type = http.param("type")?.let {
   728                 try {RelationType.valueOf(it)}
   729                 catch (_:IllegalArgumentException) {null}
   730             }
   731             if (type == null) {
   732                 http.response.sendError(500)
   733                 return
   734             }
   735             val rel = http.param("to")?.toIntOrNull()?.let(dao::findIssue)?.let {
   736                 IssueRelation(
   737                     issue,
   738                     it,
   739                     type,
   740                     http.param("reverse")?.toBoolean() ?: false
   741                 )
   742             }
   744             // execute removal, if there is something to remove
   745             rel?.run(dao::deleteIssueRelation)
   747             // always pretend that the operation was successful - if there was nothing to remove, it's okay
   748             http.renderCommit("${issuesHref}${issue.id}")
   749         }
   750     }
   751 }

mercurial