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: }