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

changeset 292
703591e739f4
parent 284
671c1c8fbf1c
equal deleted inserted replaced
291:bcf05cccac6f 292:703591e739f4
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */ 24 */
25 25
26 package de.uapcore.lightpit.servlet 26 package de.uapcore.lightpit.servlet
27 27
28 import de.uapcore.lightpit.AbstractServlet 28 import de.uapcore.lightpit.*
29 import de.uapcore.lightpit.HttpRequest
30 import de.uapcore.lightpit.boolValidator
31 import de.uapcore.lightpit.dao.DataAccessObject 29 import de.uapcore.lightpit.dao.DataAccessObject
32 import de.uapcore.lightpit.dateOptValidator
33 import de.uapcore.lightpit.entities.* 30 import de.uapcore.lightpit.entities.*
34 import de.uapcore.lightpit.types.* 31 import de.uapcore.lightpit.types.*
35 import de.uapcore.lightpit.viewmodel.* 32 import de.uapcore.lightpit.viewmodel.*
36 import jakarta.servlet.annotation.WebServlet 33 import jakarta.servlet.annotation.WebServlet
37 import java.sql.Date 34 import java.sql.Date
84 styleSheets = listOf("projects") 81 styleSheets = listOf("projects")
85 render("projects") 82 render("projects")
86 } 83 }
87 } 84 }
88 85
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 )
103
104 private sealed interface LookupResult<T>
105 private class NotFound<T> : LookupResult<T>
106 private data class Found<T>(val elem: T?) : LookupResult<T>
107
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 }
121
122 private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? {
123 val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null
124
125 val versions: List<Version> = dao.listVersions(project)
126 val components: List<Component> = dao.listComponents(project)
127
128 return ProjectInfo(
129 project,
130 versions,
131 components,
132 dao.collectIssueSummary(project)
133 )
134 }
135
136 private fun sanitizeNode(name: String): String { 86 private fun sanitizeNode(name: String): String {
137 val san = name.replace(Regex("[/\\\\]"), "-") 87 val san = name.replace(Regex("[/\\\\]"), "-")
138 return if (san.startsWith(".")) { 88 return if (san.startsWith(".")) {
139 "v$san" 89 "v$san"
140 } else { 90 } else {
142 } 92 }
143 } 93 }
144 94
145 private fun feedPath(project: Project) = "feed/${project.node}/issues.rss" 95 private fun feedPath(project: Project) = "feed/${project.node}/issues.rss"
146 96
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 }
155
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 }
162
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 }
181
182 return PathInfos(projectInfo, version, component)
183 }
184
185 private fun project(http: HttpRequest, dao: DataAccessObject) { 97 private fun project(http: HttpRequest, dao: DataAccessObject) {
186 withPathInfo(http, dao)?.run { 98 withPathInfo(http, dao)?.let {path ->
99 val project = path.projectInfo.project
187 100
188 val filter = IssueFilter(http) 101 val filter = IssueFilter(http)
189 102
190 val needRelationsMap = filter.onlyBlocker 103 val needRelationsMap = filter.onlyBlocker
191 104
192 val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap() 105 val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap()
193 106
194 val issues = dao.listIssues(project, filter.includeDone, version, component) 107 val specificVersion = path.versionInfo !is OptionalPathInfo.All
108 val version = if (path.versionInfo is OptionalPathInfo.Specific) path.versionInfo.elem else null
109 val specificComponent = path.componentInfo !is OptionalPathInfo.All
110 val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null
111
112 val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component)
195 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) 113 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary))
196 .filter { 114 .filter {
197 (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) && 115 (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) &&
198 (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) && 116 (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) &&
199 (filter.status.isEmpty() || filter.status.contains(it.status)) && 117 (filter.status.isEmpty() || filter.status.contains(it.status)) &&
200 (filter.category.isEmpty() || filter.category.contains(it.category)) 118 (filter.category.isEmpty() || filter.category.contains(it.category))
201 } 119 }
202 120
203 with(http) { 121 with(http) {
204 pageTitle = project.name 122 pageTitle = project.name
205 view = ProjectDetails(projectInfo, issues, filter, version, component) 123 view = ProjectDetails(path, issues, filter)
206 feedPath = feedPath(project) 124 feedPath = feedPath(project)
207 navigationMenu = activeProjectNavMenu( 125 navigationMenu = projectNavMenu(dao.listProjects(), path)
208 dao.listProjects(),
209 projectInfo,
210 version,
211 component
212 )
213 styleSheets = listOf("projects") 126 styleSheets = listOf("projects")
214 javascript = "project-details" 127 javascript = "project-details"
215 render("project-details") 128 render("project-details")
216 } 129 }
217 } 130 }
218 } 131 }
219 132
220 private fun projectForm(http: HttpRequest, dao: DataAccessObject) { 133 private fun projectForm(http: HttpRequest, dao: DataAccessObject) {
134 http.styleSheets = listOf("projects")
221 if (!http.pathParams.containsKey("project")) { 135 if (!http.pathParams.containsKey("project")) {
222 http.view = ProjectEditView(Project(-1), dao.listUsers()) 136 http.view = ProjectEditView(Project(-1), dao.listUsers())
223 http.navigationMenu = projectNavMenu(dao.listProjects()) 137 http.navigationMenu = projectNavMenu(dao.listProjects())
138 http.render("project-form")
224 } else { 139 } else {
225 val projectInfo = obtainProjectInfo(http, dao) 140 withPathInfo(http, dao)?.let { path ->
226 if (projectInfo == null) { 141 http.view = ProjectEditView(path.projectInfo.project, dao.listUsers())
227 http.response.sendError(404) 142 http.navigationMenu = projectNavMenu(dao.listProjects(), path)
228 return 143 http.render("project-form")
229 } 144 }
230 http.view = ProjectEditView(projectInfo.project, dao.listUsers()) 145 }
231 http.navigationMenu = activeProjectNavMenu(
232 dao.listProjects(),
233 projectInfo
234 )
235 }
236 http.styleSheets = listOf("projects")
237 http.render("project-form")
238 } 146 }
239 147
240 private fun projectCommit(http: HttpRequest, dao: DataAccessObject) { 148 private fun projectCommit(http: HttpRequest, dao: DataAccessObject) {
241 val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply { 149 val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply {
242 name = http.param("name") ?: "" 150 name = http.param("name") ?: ""
262 170
263 http.renderCommit("projects/${project.node}") 171 http.renderCommit("projects/${project.node}")
264 } 172 }
265 173
266 private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) { 174 private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) {
267 val projectInfo = obtainProjectInfo(http, dao) 175 withPathInfo(http, dao)?.let { path ->
268 if (projectInfo == null) { 176 // if analysis is not configured, reject the request
269 http.response.sendError(404) 177 if (path.projectInfo.project.vcs == VcsType.None) {
270 return 178 http.response.sendError(404)
271 } 179 return
272 180 }
273 // if analysis is not configured, reject the request 181
274 if (projectInfo.project.vcs == VcsType.None) { 182 // obtain the list of issues for this project to filter cross-project references
275 http.response.sendError(404) 183 val knownIds = dao.listIssues(path.projectInfo.project, true).map { it.id }
276 return 184
277 } 185 // read the provided commit log and merge only the refs that relate issues from the current project
278 186 dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) })
279 // obtain the list of issues for this project to filter cross-project references 187 }
280 val knownIds = dao.listIssues(projectInfo.project, true).map { it.id }
281
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 } 188 }
285 189
286 private fun versions(http: HttpRequest, dao: DataAccessObject) { 190 private fun versions(http: HttpRequest, dao: DataAccessObject) {
287 val projectInfo = obtainProjectInfo(http, dao) 191 withPathInfo(http, dao)?.let { path ->
288 if (projectInfo == null) { 192 with(http) {
289 http.response.sendError(404) 193 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}"
290 return 194 view = VersionsView(
291 } 195 path.projectInfo,
292 196 dao.listVersionSummaries(path.projectInfo.project)
293 with(http) { 197 )
294 pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.versions")}" 198 feedPath = feedPath(path.projectInfo.project)
295 view = VersionsView( 199 navigationMenu = projectNavMenu(dao.listProjects(), path)
296 projectInfo, 200 styleSheets = listOf("projects")
297 dao.listVersionSummaries(projectInfo.project) 201 javascript = "project-details"
298 ) 202 render("versions")
299 feedPath = feedPath(projectInfo.project) 203 }
300 navigationMenu = activeProjectNavMenu(
301 dao.listProjects(),
302 projectInfo
303 )
304 styleSheets = listOf("projects")
305 javascript = "project-details"
306 render("versions")
307 } 204 }
308 } 205 }
309 206
310 private fun versionForm(http: HttpRequest, dao: DataAccessObject) { 207 private fun versionForm(http: HttpRequest, dao: DataAccessObject) {
311 val projectInfo = obtainProjectInfo(http, dao) 208 withPathInfo(http, dao)?.let { path ->
312 if (projectInfo == null) { 209 val version = if (path.versionInfo is OptionalPathInfo.Specific)
313 http.response.sendError(404) 210 path.versionInfo.elem else Version(-1, path.projectInfo.project.id)
314 return 211
315 } 212 with(http) {
316 213 view = VersionEditView(path.projectInfo, version)
317 val version: Version 214 feedPath = feedPath(path.projectInfo.project)
318 when (val result = http.lookupPathParam("version", projectInfo.versions)) { 215 navigationMenu = projectNavMenu(dao.listProjects(), path)
319 is NotFound -> { 216 styleSheets = listOf("projects")
320 http.response.sendError(404) 217 render("version-form")
321 return 218 }
322 }
323 is Found -> {
324 version = result.elem ?: Version(-1, projectInfo.project.id)
325 }
326 }
327
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 } 219 }
339 } 220 }
340 221
341 private fun obtainIdAndProject(http: HttpRequest, dao: DataAccessObject): Pair<Int, Project>? { 222 private fun obtainIdAndProject(http: HttpRequest, dao: DataAccessObject): Pair<Int, Project>? {
342 val id = http.param("id")?.toIntOrNull() 223 val id = http.param("id")?.toIntOrNull()
383 264
384 http.renderCommit("projects/${project.node}/versions/") 265 http.renderCommit("projects/${project.node}/versions/")
385 } 266 }
386 267
387 private fun components(http: HttpRequest, dao: DataAccessObject) { 268 private fun components(http: HttpRequest, dao: DataAccessObject) {
388 val projectInfo = obtainProjectInfo(http, dao) 269 withPathInfo(http, dao)?.let { path ->
389 if (projectInfo == null) { 270 with(http) {
390 http.response.sendError(404) 271 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}"
391 return 272 view = ComponentsView(
392 } 273 path.projectInfo,
393 274 dao.listComponentSummaries(path.projectInfo.project)
394 with(http) { 275 )
395 pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.components")}" 276 feedPath = feedPath(path.projectInfo.project)
396 view = ComponentsView( 277 navigationMenu = projectNavMenu(dao.listProjects(), path)
397 projectInfo, 278 styleSheets = listOf("projects")
398 dao.listComponentSummaries(projectInfo.project) 279 javascript = "project-details"
399 ) 280 render("components")
400 feedPath = feedPath(projectInfo.project) 281 }
401 navigationMenu = activeProjectNavMenu(
402 dao.listProjects(),
403 projectInfo
404 )
405 styleSheets = listOf("projects")
406 javascript = "project-details"
407 render("components")
408 } 282 }
409 } 283 }
410 284
411 private fun componentForm(http: HttpRequest, dao: DataAccessObject) { 285 private fun componentForm(http: HttpRequest, dao: DataAccessObject) {
412 val projectInfo = obtainProjectInfo(http, dao) 286 withPathInfo(http, dao)?.let { path ->
413 if (projectInfo == null) { 287 val component = if (path.componentInfo is OptionalPathInfo.Specific)
414 http.response.sendError(404) 288 path.componentInfo.elem else Component(-1, path.projectInfo.project.id)
415 return 289
416 } 290 with(http) {
417 291 view = ComponentEditView(path.projectInfo, component, dao.listUsers())
418 val component: Component 292 feedPath = feedPath(path.projectInfo.project)
419 when (val result = http.lookupPathParam("component", projectInfo.components)) { 293 navigationMenu = projectNavMenu(dao.listProjects(), path)
420 is NotFound -> { 294 styleSheets = listOf("projects")
421 http.response.sendError(404) 295 render("component-form")
422 return 296 }
423 }
424 is Found -> {
425 component = result.elem ?: Component(-1, projectInfo.project.id)
426 }
427 }
428
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 } 297 }
440 } 298 }
441 299
442 private fun componentCommit(http: HttpRequest, dao: DataAccessObject) { 300 private fun componentCommit(http: HttpRequest, dao: DataAccessObject) {
443 val idParams = obtainIdAndProject(http, dao) ?: return 301 val idParams = obtainIdAndProject(http, dao) ?: return
482 http: HttpRequest, 340 http: HttpRequest,
483 dao: DataAccessObject, 341 dao: DataAccessObject,
484 issue: Issue, 342 issue: Issue,
485 relationError: String? = null 343 relationError: String? = null
486 ) { 344 ) {
487 withPathInfo(http, dao)?.run { 345 withPathInfo(http, dao)?.let {path ->
488 val comments = dao.listComments(issue) 346 val comments = dao.listComments(issue)
489 347
490 with(http) { 348 with(http) {
491 pageTitle = "${projectInfo.project.name}: #${issue.id} ${issue.subject}" 349 pageTitle = "${path.projectInfo.project.name}: #${issue.id} ${issue.subject}"
492 view = IssueDetailView( 350 view = IssueDetailView(
351 path,
493 issue, 352 issue,
494 comments, 353 comments,
495 project, 354 path.projectInfo.project,
496 version, 355 dao.listIssues(path.projectInfo.project, true),
497 component,
498 dao.listIssues(project, true),
499 dao.listIssueRelations(issue), 356 dao.listIssueRelations(issue),
500 relationError, 357 relationError,
501 dao.listCommitRefs(issue) 358 dao.listCommitRefs(issue)
502 ) 359 )
503 feedPath = feedPath(projectInfo.project) 360 feedPath = feedPath(path.projectInfo.project)
504 navigationMenu = activeProjectNavMenu( 361 navigationMenu = projectNavMenu(dao.listProjects(), path)
505 dao.listProjects(),
506 projectInfo,
507 version,
508 component
509 )
510 styleSheets = listOf("projects") 362 styleSheets = listOf("projects")
511 javascript = "issue-editor" 363 javascript = "issue-editor"
512 render("issue-view") 364 render("issue-view")
513 } 365 }
514 } 366 }
515 } 367 }
516 368
517 private fun issueForm(http: HttpRequest, dao: DataAccessObject) { 369 private fun issueForm(http: HttpRequest, dao: DataAccessObject) {
518 withPathInfo(http, dao)?.run { 370 withPathInfo(http, dao)?.let { path ->
519 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue( 371 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue(
520 -1, 372 -1,
521 project, 373 path.projectInfo.project,
522 ) 374 )
523 375
524 // for new issues set some defaults 376 // for new issues set some defaults
525 if (issue.id < 0) { 377 if (issue.id < 0) {
526 // pre-select component, if available in the path info 378 // pre-select component, if available in the path info
527 issue.component = component 379 if (path.componentInfo is OptionalPathInfo.Specific) {
380 issue.component = path.componentInfo.elem
381 }
528 382
529 // pre-select version, if available in the path info 383 // pre-select version, if available in the path info
530 if (version != null) { 384 if (path.versionInfo is OptionalPathInfo.Specific) {
531 if (version.status.isReleased) { 385 if (path.versionInfo.elem.status.isReleased) {
532 issue.affected = version 386 issue.affected = path.versionInfo.elem
533 } else { 387 } else {
534 issue.resolved = version 388 issue.resolved = path.versionInfo.elem
535 } 389 }
536 } 390 }
537 } 391 }
538 392
539 with(http) { 393 with(http) {
540 view = IssueEditView( 394 view = IssueEditView(
541 issue, 395 issue,
542 projectInfo.versions, 396 path.projectInfo.versions,
543 projectInfo.components, 397 path.projectInfo.components,
544 dao.listUsers(), 398 dao.listUsers(),
545 project, 399 path.projectInfo.project,
546 version, 400 path
547 component
548 ) 401 )
549 feedPath = feedPath(projectInfo.project) 402 feedPath = feedPath(path.projectInfo.project)
550 navigationMenu = activeProjectNavMenu( 403 navigationMenu = projectNavMenu(dao.listProjects(), path)
551 dao.listProjects(),
552 projectInfo,
553 version,
554 component
555 )
556 styleSheets = listOf("projects") 404 styleSheets = listOf("projects")
557 javascript = "issue-editor" 405 javascript = "issue-editor"
558 render("issue-form") 406 render("issue-form")
559 } 407 }
560 } 408 }
604 452
605 private fun issueCommit(http: HttpRequest, dao: DataAccessObject) { 453 private fun issueCommit(http: HttpRequest, dao: DataAccessObject) {
606 withPathInfo(http, dao)?.run { 454 withPathInfo(http, dao)?.run {
607 val issue = Issue( 455 val issue = Issue(
608 http.param("id")?.toIntOrNull() ?: -1, 456 http.param("id")?.toIntOrNull() ?: -1,
609 project 457 projectInfo.project
610 ).apply { 458 ).apply {
611 component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1) 459 component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
612 category = IssueCategory.valueOf(http.param("category") ?: "") 460 category = IssueCategory.valueOf(http.param("category") ?: "")
613 status = IssueStatus.valueOf(http.param("status") ?: "") 461 status = IssueStatus.valueOf(http.param("status") ?: "")
614 subject = http.param("subject") ?: "" 462 subject = http.param("subject") ?: ""

mercurial