Sat, 27 Nov 2021 13:03:57 +0100
#109 add comment history
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 com.github.difflib.text.DiffRow
29 import com.github.difflib.text.DiffRowGenerator
30 import de.uapcore.lightpit.AbstractServlet
31 import de.uapcore.lightpit.HttpRequest
32 import de.uapcore.lightpit.dao.DataAccessObject
33 import de.uapcore.lightpit.entities.IssueCommentHistoryEntry
34 import de.uapcore.lightpit.entities.IssueHistoryEntry
35 import de.uapcore.lightpit.types.IssueHistoryType
36 import de.uapcore.lightpit.viewmodel.CommentDiff
37 import de.uapcore.lightpit.viewmodel.IssueDiff
38 import de.uapcore.lightpit.viewmodel.IssueFeed
39 import de.uapcore.lightpit.viewmodel.IssueFeedEntry
40 import java.text.SimpleDateFormat
41 import javax.servlet.annotation.WebServlet
44 @WebServlet(urlPatterns = ["/feed/*"])
45 class FeedServlet : AbstractServlet() {
47 init {
48 get("/%project/issues.rss", this::issues)
49 }
51 val diffGenerator by lazyOf(DiffRowGenerator.create()
52 .showInlineDiffs(true)
53 .mergeOriginalRevised(true)
54 .inlineDiffByWord(true)
55 .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" }
56 .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" }
57 .build()
58 )
60 private fun fullContent(data: IssueCommentHistoryEntry) =
61 CommentDiff(
62 data.issueid,
63 data.commentid,
64 data.subject,
65 data.comment.replace("\r", "")
66 )
68 private fun diffContent(cur: IssueCommentHistoryEntry, next: IssueCommentHistoryEntry) =
69 CommentDiff(
70 cur.issueid,
71 cur.commentid,
72 cur.subject,
73 diffGenerator.generateDiffRows(
74 next.comment.replace("\r", "").split('\n'),
75 cur.comment.replace("\r", "").split('\n')
76 ).joinToString("\n", transform = DiffRow::getOldLine)
77 )
79 private fun fullContent(issue: IssueHistoryEntry) = IssueDiff(
80 issue.issueid,
81 issue.subject,
82 issue.component,
83 issue.status.name,
84 issue.category.name,
85 issue.subject,
86 issue.description.replace("\r", ""),
87 issue.assignee,
88 issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "",
89 issue.affected,
90 issue.resolved
91 )
93 private fun diffContent(cur: IssueHistoryEntry, next: IssueHistoryEntry): IssueDiff {
94 val prev = fullContent(next)
95 val diff = fullContent(cur)
96 val result = diffGenerator.generateDiffRows(
97 listOf(
98 prev.subject, prev.component, prev.status,
99 prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved
100 ),
101 listOf(
102 diff.subject, diff.component, diff.status,
103 diff.category, diff.assignee, diff.eta, diff.affected, diff.resolved
104 )
105 )
107 diff.subject = result[0].oldLine
108 diff.component = result[1].oldLine
109 diff.status = result[2].oldLine
110 diff.category = result[3].oldLine
111 diff.assignee = result[4].oldLine
112 diff.eta = result[5].oldLine
113 diff.affected = result[6].oldLine
114 diff.resolved = result[7].oldLine
116 diff.description = diffGenerator.generateDiffRows(
117 prev.description.split('\n'),
118 diff.description.split('\n')
119 ).joinToString("\n", transform = DiffRow::getOldLine)
121 return diff
122 }
124 /**
125 * Generates the feed entries.
126 * Assumes that [issueEntries] and [commentEntries] are already sorted by timestamp (descending).
127 */
128 private fun generateFeedEntries(
129 issueEntries: List<IssueHistoryEntry>,
130 commentEntries: List<IssueCommentHistoryEntry>
131 ): List<IssueFeedEntry> =
132 (generateIssueFeedEntries(issueEntries) + generateCommentFeedEntries(commentEntries)).sortedByDescending { it.time }
134 private fun generateIssueFeedEntries(entries: List<IssueHistoryEntry>): List<IssueFeedEntry> =
135 if (entries.isEmpty()) {
136 emptyList()
137 } else {
138 entries.groupBy { it.issueid }.mapValues { (_, history) ->
139 history.zipWithNext().map { (cur, next) ->
140 IssueFeedEntry(
141 cur.time, cur.type, issue = diffContent(cur, next)
142 )
143 }.plus(
144 history.last().let { IssueFeedEntry(it.time, it.type, issue = fullContent(it)) }
145 )
146 }.flatMap { it.value }
147 }
149 private fun generateCommentFeedEntries(entries: List<IssueCommentHistoryEntry>): List<IssueFeedEntry> =
150 if (entries.isEmpty()) {
151 emptyList()
152 } else {
153 entries.groupBy { it.commentid }.mapValues { (_, history) ->
154 history.zipWithNext().map { (cur, next) ->
155 IssueFeedEntry(
156 cur.time, cur.type, comment = diffContent(cur, next)
157 )
158 }.plus(
159 history.last().let { IssueFeedEntry(it.time, it.type, comment = fullContent(it)) }
160 )
161 }.flatMap { it.value }
162 }
164 private fun issues(http: HttpRequest, dao: DataAccessObject) {
165 val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) }
166 if (project == null) {
167 http.response.sendError(404)
168 return
169 }
170 val assignees = http.param("assignee")?.split(',')
171 val comments = http.param("comments") ?: "all"
173 val days = http.param("days")?.toIntOrNull() ?: 30
175 val issuesFromDb = dao.listIssueHistory(project.id, days)
176 val issueHistory = if (assignees == null) issuesFromDb else
177 issuesFromDb.filter { assignees.contains(it.currentAssignee) }
179 val commentsFromDb = dao.listIssueCommentHistory(project.id, days)
180 val commentHistory = when (comments) {
181 "all" -> commentsFromDb
182 "new" -> commentsFromDb.filter { it.type == IssueHistoryType.NewComment }
183 else -> emptyList()
184 }
186 http.view = IssueFeed(project, generateFeedEntries(issueHistory, commentHistory))
187 http.renderFeed("issues-feed")
188 }
189 }