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

Mon, 05 Aug 2024 18:40:47 +0200

author
Mike Becker <universe@uap-core.de>
date
Mon, 05 Aug 2024 18:40:47 +0200
changeset 311
bf67e0ff7131
parent 260
fb2ae2d63a56
child 315
4fe3f61744cf
permissions
-rw-r--r--

add new global issues page - fixes #404

/*
 * 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 com.github.difflib.text.DiffRow
import com.github.difflib.text.DiffRowGenerator
import de.uapcore.lightpit.AbstractServlet
import de.uapcore.lightpit.HttpRequest
import de.uapcore.lightpit.dao.DataAccessObject
import de.uapcore.lightpit.entities.IssueCommentHistoryEntry
import de.uapcore.lightpit.entities.IssueHistoryEntry
import de.uapcore.lightpit.entities.Project
import de.uapcore.lightpit.types.IssueHistoryType
import de.uapcore.lightpit.viewmodel.CommentDiff
import de.uapcore.lightpit.viewmodel.IssueDiff
import de.uapcore.lightpit.viewmodel.IssueFeed
import de.uapcore.lightpit.viewmodel.IssueFeedEntry
import jakarta.servlet.annotation.WebServlet
import java.text.SimpleDateFormat


@WebServlet(urlPatterns = ["/feed/*"])
class FeedServlet : AbstractServlet() {

    init {
        get("/%project/issues.rss", this::issues)
    }

    private val diffGenerator: DiffRowGenerator by lazyOf(DiffRowGenerator.create()
        .showInlineDiffs(true)
        .mergeOriginalRevised(true)
        .inlineDiffByWord(true)
        .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" }
        .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" }
        .build()
    )

    private fun fullContent(data: IssueCommentHistoryEntry) =
        CommentDiff(
            data.issueid,
            data.commentid,
            data.subject,
            data.comment.replace("\r", "")
        )

    private fun diffContent(cur: IssueCommentHistoryEntry, next: IssueCommentHistoryEntry) =
        CommentDiff(
            cur.issueid,
            cur.commentid,
            cur.subject,
            diffGenerator.generateDiffRows(
                next.comment.replace("\r", "").split('\n'),
                cur.comment.replace("\r", "").split('\n')
            ).joinToString("\n", transform = DiffRow::getOldLine)
        )

    private fun fullContent(issue: IssueHistoryEntry) = IssueDiff(
        issue.issueid,
        issue.subject,
        issue.project,
        issue.component,
        issue.status.name,
        issue.category.name,
        issue.subject,
        issue.description.replace("\r", ""),
        issue.assignee,
        issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "",
        issue.affected,
        issue.resolved
    )

    private fun diffContent(cur: IssueHistoryEntry, next: IssueHistoryEntry): IssueDiff {
        val prev = fullContent(next)
        val diff = fullContent(cur)
        val result = diffGenerator.generateDiffRows(
            listOf(
                prev.subject, prev.component, prev.status,
                prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved
            ),
            listOf(
                diff.subject, diff.component, diff.status,
                diff.category, diff.assignee, diff.eta, diff.affected, diff.resolved
            )
        )

        diff.subject = result[0].oldLine
        diff.component = result[1].oldLine
        diff.status = result[2].oldLine
        diff.category = result[3].oldLine
        diff.assignee = result[4].oldLine
        diff.eta = result[5].oldLine
        diff.affected = result[6].oldLine
        diff.resolved = result[7].oldLine

        diff.description = diffGenerator.generateDiffRows(
            prev.description.split('\n'),
            diff.description.split('\n')
        ).joinToString("\n", transform = DiffRow::getOldLine)

        return diff
    }

    /**
     * Generates the feed entries.
     * Assumes that [issueEntries] and [commentEntries] are already sorted by timestamp (descending).
     */
    private fun generateFeedEntries(
        issueEntries: List<IssueHistoryEntry>,
        commentEntries: List<IssueCommentHistoryEntry>
    ): List<IssueFeedEntry> =
        (generateIssueFeedEntries(issueEntries) + generateCommentFeedEntries(commentEntries)).sortedByDescending { it.time }

    private fun generateIssueFeedEntries(entries: List<IssueHistoryEntry>): List<IssueFeedEntry> =
        if (entries.isEmpty()) {
            emptyList()
        } else {
            entries.groupBy { it.issueid }.mapValues { (_, history) ->
                history.zipWithNext().map { (cur, next) ->
                    IssueFeedEntry(
                        cur.time, cur.type, issue = diffContent(cur, next)
                    )
                }.plus(
                    history.last().let { IssueFeedEntry(it.time, it.type, issue = fullContent(it)) }
                )
            }.flatMap { it.value }
        }

    private fun generateCommentFeedEntries(entries: List<IssueCommentHistoryEntry>): List<IssueFeedEntry> =
        if (entries.isEmpty()) {
            emptyList()
        } else {
            entries.groupBy { it.commentid }.mapValues { (_, history) ->
                history.zipWithNext().map { (cur, next) ->
                    IssueFeedEntry(
                        cur.time, cur.type, comment = diffContent(cur, next)
                    )
                }.plus(
                    history.last().let { IssueFeedEntry(it.time, it.type, comment = fullContent(it)) }
                )
            }.flatMap { it.value }
        }

    private fun issues(http: HttpRequest, dao: DataAccessObject) {
        val projectNode = http.pathParams["project"].orEmpty()
        val project: Project?
        if (projectNode == "-") {
            project = null
        } else {
            project = dao.findProjectByNode(projectNode)
            if (project == null) {
                http.response.sendError(404)
                return
            }
        }
        val assignees = http.param("assignee")?.split(',')
        val comments = http.param("comments") ?: "all"

        val days = http.param("days")?.toIntOrNull() ?: 30

        val issuesFromDb = dao.listIssueHistory(project, days)
        val issueHistory = if (assignees == null) issuesFromDb else
            issuesFromDb.filter { assignees.contains(it.currentAssignee) }

        val commentsFromDb = dao.listIssueCommentHistory(project, days)
        val commentHistory = when (comments) {
            "all" -> commentsFromDb
            "new" -> commentsFromDb.filter { it.type == IssueHistoryType.NewComment }
            else -> emptyList()
        }

        http.view = IssueFeed(project, generateFeedEntries(issueHistory, commentHistory))
        http.renderFeed("issues-feed")
    }
}

mercurial