universe@195: /*
universe@195: * Copyright 2021 Mike Becker. All rights reserved.
universe@195: *
universe@195: * Redistribution and use in source and binary forms, with or without
universe@195: * modification, are permitted provided that the following conditions are met:
universe@195: *
universe@195: * 1. Redistributions of source code must retain the above copyright
universe@195: * notice, this list of conditions and the following disclaimer.
universe@195: *
universe@195: * 2. Redistributions in binary form must reproduce the above copyright
universe@195: * notice, this list of conditions and the following disclaimer in the
universe@195: * documentation and/or other materials provided with the distribution.
universe@195: *
universe@195: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
universe@195: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
universe@195: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
universe@195: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
universe@195: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
universe@195: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
universe@195: * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
universe@195: * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
universe@195: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
universe@195: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
universe@195: */
universe@195:
universe@195: package de.uapcore.lightpit.servlet
universe@195:
universe@236: import com.github.difflib.text.DiffRow
universe@236: import com.github.difflib.text.DiffRowGenerator
universe@195: import de.uapcore.lightpit.AbstractServlet
universe@195: import de.uapcore.lightpit.HttpRequest
universe@195: import de.uapcore.lightpit.dao.DataAccessObject
universe@235: import de.uapcore.lightpit.entities.IssueHistoryData
universe@235: import de.uapcore.lightpit.entities.IssueHistoryEntry
universe@235: import de.uapcore.lightpit.viewmodel.IssueDiff
universe@195: import de.uapcore.lightpit.viewmodel.IssueFeed
universe@235: import de.uapcore.lightpit.viewmodel.IssueFeedEntry
universe@235: import java.text.SimpleDateFormat
universe@195: import javax.servlet.annotation.WebServlet
universe@195:
universe@236:
universe@195: @WebServlet(urlPatterns = ["/feed/*"])
universe@195: class FeedServlet : AbstractServlet() {
universe@195:
universe@195: init {
universe@198: get("/%project/issues.rss", this::issues)
universe@198: }
universe@198:
universe@235: private fun fullContent(issue: IssueHistoryData) = IssueDiff(
universe@235: issue.id,
universe@235: issue.subject,
universe@235: issue.component,
universe@235: issue.status.name,
universe@235: issue.category.name,
universe@235: issue.subject,
universe@236: issue.description.replace("\r", ""),
universe@235: issue.assignee,
universe@235: issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "",
universe@235: issue.affected,
universe@235: issue.resolved
universe@235: )
universe@235:
universe@235: private fun diffContent(cur: IssueHistoryData, next: IssueHistoryData): IssueDiff {
universe@236: val generator = DiffRowGenerator.create()
universe@236: .showInlineDiffs(true)
universe@236: .mergeOriginalRevised(true)
universe@236: .inlineDiffByWord(true)
universe@236: .oldTag { start -> if (start) "" else "" }
universe@236: .newTag { start -> if (start) "" else "" }
universe@236: .build()
universe@236:
universe@235: val prev = fullContent(next)
universe@235: val diff = fullContent(cur)
universe@236:
universe@236: val result = generator.generateDiffRows(
universe@238: listOf(
universe@238: prev.subject, prev.component, prev.status,
universe@238: prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved
universe@238: ),
universe@238: listOf(
universe@238: diff.subject, diff.component, diff.status,
universe@238: diff.category, diff.assignee, diff.eta, diff.affected, diff.resolved
universe@238: )
universe@236: )
universe@236:
universe@236: diff.subject = result[0].oldLine
universe@236: diff.component = result[1].oldLine
universe@236: diff.status = result[2].oldLine
universe@236: diff.category = result[3].oldLine
universe@236: diff.assignee = result[4].oldLine
universe@236: diff.eta = result[5].oldLine
universe@236: diff.affected = result[6].oldLine
universe@236: diff.resolved = result[7].oldLine
universe@236:
universe@236: diff.description = generator.generateDiffRows(
universe@236: prev.description.split('\n'),
universe@236: diff.description.split('\n')
universe@240: ).joinToString("\n", transform = DiffRow::getOldLine)
universe@236:
universe@235: return diff
universe@235: }
universe@235:
universe@235: /**
universe@235: * Generates the feed entries.
universe@235: * Assumes that [historyEntry] is already sorted by timestamp (descending).
universe@235: */
universe@238: private fun generateFeedEntries(historyEntry: List): List =
universe@238: if (historyEntry.isEmpty()) {
universe@238: emptyList()
universe@238: } else {
universe@238: historyEntry.groupBy { it.data.id }.mapValues { (_, history) ->
universe@238: history.zipWithNext().map { (cur, next) ->
universe@238: IssueFeedEntry(
universe@238: cur.time, cur.type, diffContent(cur.data, next.data)
universe@238: )
universe@238: }.plus(
universe@238: history.last().let { IssueFeedEntry(it.time, it.type, fullContent(it.data)) }
universe@238: )
universe@238: }.flatMap { it.value }.sortedByDescending { it.time }
universe@238: }
universe@235:
universe@195: private fun issues(http: HttpRequest, dao: DataAccessObject) {
universe@198: val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) }
universe@198: if (project == null) {
universe@198: http.response.sendError(404)
universe@198: return
universe@198: }
universe@239: val assignees = http.param("assignee")?.split(',')
universe@195:
universe@235: val days = http.param("days")?.toIntOrNull() ?: 30
universe@195:
universe@239: val issuesFromDb = dao.listIssueHistory(project.id, days)
universe@239: val issueHistory = if (assignees == null) issuesFromDb else
universe@239: issuesFromDb.filter { assignees.contains(it.data.assigneeUsername) }
universe@239:
universe@235: // TODO: add comment history depending on parameter
universe@235:
universe@235: http.view = IssueFeed(project, generateFeedEntries(issueHistory))
universe@198: http.renderFeed("issues-feed")
universe@195: }
universe@195: }