Thu, 13 May 2021 19:31:09 +0200
fixes wrong handling of feeds - only one channel per feed is allowed
--- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt Thu May 13 18:01:56 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt Thu May 13 19:31:09 2021 +0200 @@ -62,6 +62,11 @@ const val REQ_ATTR_BASE_HREF = "base_href" /** + * Key for the request attribute containing the RSS feed href. + */ + const val REQ_ATTR_FEED_HREF = "feed_href" + + /** * Key for the request attribute containing the full path information (servlet path + path info). */ const val REQ_ATTR_PATH = "requestPath"
--- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Thu May 13 18:01:56 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Thu May 13 19:31:09 2021 +0200 @@ -86,6 +86,12 @@ request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value) } + var feedPath = "" + set(value) { + field = value + request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value) + } + /** * The view object. * @@ -98,9 +104,21 @@ } /** + * Additional port info, if necessary. + */ + private val portInfo = + if ((request.scheme == "http" && request.serverPort == 80) + || (request.scheme == "https" && request.serverPort == 443) + ) "" else ":${request.serverPort}" + + /** * The base path of this application. */ - val baseHref get() = "${request.scheme}://${request.serverName}:${request.serverPort}${request.contextPath}/" + val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/" + + init { + feedPath = "feed/projects.rss" + } private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext) private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp") @@ -108,10 +126,15 @@ fun param(name: String): String? = request.getParameter(name) fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray() - fun forward(jsp: String) { + private fun forward(jsp: String) { request.getRequestDispatcher(jspPath(jsp)).forward(request, response) } + fun renderFeed(page: String? = null) { + page?.let { contentPage = it } + forward("feed") + } + fun render(page: String? = null) { page?.let { contentPage = it } forward("site")
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt Thu May 13 18:01:56 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt Thu May 13 19:31:09 2021 +0200 @@ -28,24 +28,39 @@ import de.uapcore.lightpit.AbstractServlet import de.uapcore.lightpit.HttpRequest import de.uapcore.lightpit.dao.DataAccessObject -import de.uapcore.lightpit.entities.Issue import de.uapcore.lightpit.util.IssueFilter import de.uapcore.lightpit.util.IssueSorter +import de.uapcore.lightpit.util.SpecificFilter import de.uapcore.lightpit.viewmodel.IssueFeed +import de.uapcore.lightpit.viewmodel.ProjectFeed import javax.servlet.annotation.WebServlet @WebServlet(urlPatterns = ["/feed/*"]) class FeedServlet : AbstractServlet() { init { - get("/issues.rss", this::issues) + get("/projects.rss", this::projects) + get("/%project/issues.rss", this::issues) + } + + private fun projects(http: HttpRequest, dao: DataAccessObject) { + + val projects = dao.listProjects() + + http.view = ProjectFeed(projects) + http.renderFeed("project-feed") } private fun issues(http: HttpRequest, dao: DataAccessObject) { - - val issues = dao.listIssues(IssueFilter()).sortedWith(IssueSorter.DEFAULT_ISSUE_SORTER) + val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) } + if (project == null) { + http.response.sendError(404) + return + } - http.view = IssueFeed(issues.groupBy(Issue::project)) - http.forward("issues-feed") + val issues = dao.listIssues(IssueFilter(SpecificFilter(project))).sortedWith(IssueSorter.DEFAULT_ISSUE_SORTER) + + http.view = IssueFeed(project, issues) + http.renderFeed("issues-feed") } } \ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu May 13 18:01:56 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu May 13 19:31:09 2021 +0200 @@ -145,12 +145,15 @@ } } + private fun feedPath(project: Project) = "feed/${project.node}/issues.rss" + data class PathInfos( val projectInfo: ProjectInfo, val version: Version?, val component: Component? ) { - val issuesHref by lazyOf("projects/${projectInfo.project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/") + val project = projectInfo.project + val issuesHref by lazyOf("projects/${project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/") } private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? { @@ -186,13 +189,14 @@ withPathInfo(http, dao)?.run { val issues = dao.listIssues(IssueFilter( - project = SpecificFilter(projectInfo.project), + project = SpecificFilter(project), version = version?.let { SpecificFilter(it) } ?: AllFilter(), component = component?.let { SpecificFilter(it) } ?: AllFilter() )).sortedWith(DEFAULT_ISSUE_SORTER) with(http) { view = ProjectDetails(projectInfo, issues, version, component) + feedPath = feedPath(project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, @@ -261,6 +265,7 @@ projectInfo, dao.listVersionSummaries(projectInfo.project) ) + feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo @@ -290,6 +295,7 @@ with(http) { view = VersionEditView(projectInfo, version) + feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, @@ -342,6 +348,7 @@ projectInfo, dao.listComponentSummaries(projectInfo.project) ) + feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo @@ -371,6 +378,7 @@ with(http) { view = ComponentEditView(projectInfo, component, dao.listUsers()) + feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, @@ -426,7 +434,9 @@ val comments = dao.listComments(issue) with(http) { - view = IssueDetailView(issue, comments, projectInfo.project, version, component) + view = IssueDetailView(issue, comments, project, version, component) + // TODO: feed path for this particular issue + feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, @@ -443,7 +453,7 @@ withPathInfo(http, dao)?.run { val issue = dao.findIssue(http.pathParams["issue"]?.toIntOrNull() ?: -1) ?: Issue( -1, - projectInfo.project, + project, ) // pre-select component, if available in the path info @@ -464,10 +474,11 @@ projectInfo.versions, projectInfo.components, dao.listUsers(), - projectInfo.project, + project, version, component ) + feedPath = feedPath(projectInfo.project) navigationMenu = activeProjectNavMenu( dao.listProjects(), projectInfo, @@ -505,7 +516,7 @@ // TODO: throw validator exception instead of using defaults val issue = Issue( http.param("id")?.toIntOrNull() ?: -1, - projectInfo.project + project ).apply { component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1) category = IssueCategory.valueOf(http.param("category") ?: "") @@ -522,9 +533,9 @@ eta = http.param("eta")?.let { if (it.isBlank()) null else Date.valueOf(it) } affectedVersions = http.paramArray("affected") - .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } } + .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, project.id) } } resolvedVersions = http.paramArray("resolved") - .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, projectInfo.project.id) } } + .mapNotNull { param -> param.toIntOrNull()?.let { Version(it, project.id) } } } val openId = if (issue.id < 0) {
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt Thu May 13 18:01:56 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt Thu May 13 19:31:09 2021 +0200 @@ -28,6 +28,11 @@ import de.uapcore.lightpit.entities.Issue import de.uapcore.lightpit.entities.Project +class ProjectFeed( + val projects: List<Project> +) : View() + class IssueFeed( - val issues: Map<Project, List<Issue>> + val project: Project, + val issues: List<Issue> ) : View() \ No newline at end of file
--- a/src/main/resources/localization/strings.properties Thu May 13 18:01:56 2021 +0200 +++ b/src/main/resources/localization/strings.properties Thu May 13 19:31:09 2021 +0200 @@ -54,6 +54,11 @@ error.message = Server Message error.returnLink = Return to error.timestamp = Timestamp +feed.issues.description=Feed about recently updated issues. +feed.issues.title=LightPIT Issues +feed.projects.description=Feed about tracked projects. +feed.projects.source=Issues +feed.projects.title=LightPIT Projects issue.affected-versions=Affected Versions issue.assignee=Assignee issue.category.Bug=Bug @@ -126,6 +131,4 @@ version.status.Released=Released version.status.Unreleased=Unreleased version.status=Status -version=Version -feed.issues.title=LightPIT - Issues -feed.issues.description=Feed about recently updated issues. \ No newline at end of file +version=Version \ No newline at end of file
--- a/src/main/resources/localization/strings_de.properties Thu May 13 18:01:56 2021 +0200 +++ b/src/main/resources/localization/strings_de.properties Thu May 13 19:31:09 2021 +0200 @@ -54,6 +54,11 @@ error.message = Server Nachricht error.returnLink = Kehre zurück zu error.timestamp = Zeitstempel +feed.issues.description=Feed \u00fcber k\u00fcrzlich aktualisierte Vorg\u00e4nge. +feed.issues.title=LightPIT Vorg\u00e4nge +feed.projects.description=Feed \u00fcber verwaltete Projekte. +feed.projects.source=Vorg\u00e4nge +feed.projects.title=LightPIT Projekte issue.affected-versions=Betroffene Versionen issue.assignee=Zugewiesen issue.category.Bug=Fehler @@ -127,5 +132,3 @@ version.status.Unreleased=Unver\u00f6ffentlicht version.status=Status version=Version -feed.issues.title=LightPIT - Vorg\u00e4nge -feed.issues.description=Feed \u00fcber k\u00fcrzlich aktualisierte Vorg\u00e4nge.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/feed.jsp Thu May 13 19:31:09 2021 +0200 @@ -0,0 +1,36 @@ +<%-- + ~ 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. + --%> +<%@page contentType="application/rss+xml;charset=UTF-8" trimDirectiveWhitespaces="true" %> +<%@page import="de.uapcore.lightpit.Constants" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<c:set scope="page" var="contentPage" value="${requestScope[Constants.REQ_ATTR_CONTENT_PAGE]}"/> +<c:set scope="request" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/> +<fmt:setLocale scope="request" value="${pageContext.response.locale}"/> +<fmt:setBundle scope="request" basename="localization.strings"/> +<?xml version="1.0" encoding="utf-8"?> +<rss version="2.0"> + <c:import url="${contentPage}"/> +</rss>
--- a/src/main/webapp/WEB-INF/jsp/issues-feed.jsp Thu May 13 18:01:56 2021 +0200 +++ b/src/main/webapp/WEB-INF/jsp/issues-feed.jsp Thu May 13 19:31:09 2021 +0200 @@ -22,37 +22,24 @@ ~ 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. --%> -<%@ page contentType="application/rss+xml;charset=UTF-8" trimDirectiveWhitespaces="true" %> -<%@page import="de.uapcore.lightpit.Constants" %> +<%@page contentType="application/rss+xml;charset=UTF-8" pageEncoding="UTF-8" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueFeed" scope="request"/> -<c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/> -<fmt:setLocale scope="request" value="${pageContext.response.locale}"/> -<fmt:setBundle scope="request" basename="localization.strings"/> -<?xml version="1.0" encoding="utf-8"?> -<rss version="2.0"> - <c:forEach items="${viewmodel.issues}" var="feed"> - <c:set var="project" value="${feed.key}"/> - <c:set var="issues" value="${feed.value}"/> - <channel> - <title> - <fmt:message key="feed.issues.title"/> - <c:out value="${project.name}"/> - </title> - <link>${baseHref}projects/${project.node}/</link> - <description><fmt:message key="feed.issues.description"/></description> - <language>${pageContext.response.locale.language}</language> +<channel> + <title><c:out value="${viewmodel.project.name}"/> | <fmt:message key="feed.issues.title"/></title> + <description><fmt:message key="feed.issues.description"/></description> + <link>${baseHref}projects/${viewmodel.project.node}</link> + <language>${pageContext.response.locale.language}</language> - <c:forEach items="${issues}" var="issue"> - <item> - <title><c:if test="${not empty issue.component}"><c:out value="${issue.component.name}"/> - </c:if><c:out value="${issue.subject}"/></title> - <description><c:out value="${issue.description}"/></description> - <category><fmt:message key="issue.category.${issue.category}"/></category> - <link>${baseHref}projects/${issue.project.node}/issues/-/${empty issue.component ? '-' : issue.component.node}/${issue.id}</link> - <guid isPermaLink="true">${baseHref}projects/${issue.project.node}/issues/-/-/${issue.id}</guid> - <pubDate><fmt:formatDate value="${issue.updated}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> - </item> - </c:forEach> - </channel> + <c:forEach items="${viewmodel.issues}" var="issue"> + <item> + <title><c:if test="${not empty issue.component}"><c:out value="${issue.component.name}"/> - </c:if><c:out value="${issue.subject}"/></title> + <description><c:out value="${issue.description}"/></description> + <category><fmt:message key="issue.category.${issue.category}"/></category> + <link>${baseHref}projects/${issue.project.node}/issues/-/${empty issue.component ? '-' : issue.component.node}/${issue.id}</link> + <guid isPermaLink="true">${baseHref}projects/${issue.project.node}/issues/-/-/${issue.id}</guid> + <pubDate><fmt:formatDate value="${issue.updated}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> + </item> </c:forEach> -</rss> \ No newline at end of file +</channel>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/project-feed.jsp Thu May 13 19:31:09 2021 +0200 @@ -0,0 +1,43 @@ +<%-- + ~ 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. + --%> +<%@page contentType="application/rss+xml;charset=UTF-8" pageEncoding="UTF-8" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectFeed" scope="request"/> +<channel> + <title><fmt:message key="feed.projects.title"/></title> + <description><fmt:message key="feed.projects.description"/></description> + <link>${baseHref}projects/</link> + <language>${pageContext.response.locale.language}</language> + + <c:forEach items="${viewmodel.projects}" var="project"> + <item> + <title><c:out value="${project.name}"/></title> + <description><c:out value="${project.description}"/></description> + <link>${baseHref}projects/${project.node}</link> + <source url="${baseHref}feed/${project.node}/issues.rss"><fmt:message key="feed.projects.source"/></source> + </item> + </c:forEach> +</channel>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp Thu May 13 18:01:56 2021 +0200 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Thu May 13 19:31:09 2021 +0200 @@ -33,6 +33,9 @@ <%-- Make the base href easily available at request scope --%> <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/> +<%-- The feed URL for this page. --%> +<c:set scope="page" var="feedHref" value="${requestScope[Constants.REQ_ATTR_FEED_HREF]}"/> + <%-- Define an alias for the request path --%> <c:set scope="page" var="requestPath" value="${requestScope[Constants.REQ_ATTR_PATH]}"/> @@ -62,8 +65,7 @@ <meta http-equiv="refresh" content="0; URL=${redirectLocation}"> </c:if> <link rel="stylesheet" href="lightpit.css" type="text/css"> - <link rel="alternate" type="application/rss+xml" - title="RSS" href="${baseHref}feed/issues.rss" /> + <link rel="alternate" type="application/rss+xml" title="RSS" href="${feedHref}" /> <c:if test="${not empty extraCss}"> <c:forEach items="${extraCss}" var="cssFile"> <link rel="stylesheet" href="${cssFile}" type="text/css">