| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.plugins.changes; |
| |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.ResourceBundle; |
| |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.maven.doxia.sink.Sink; |
| import org.apache.maven.doxia.sink.SinkEventAttributes; |
| import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; |
| import org.apache.maven.doxia.util.HtmlTools; |
| import org.apache.maven.plugins.changes.model.Action; |
| import org.apache.maven.plugins.changes.model.Component; |
| import org.apache.maven.plugins.changes.model.DueTo; |
| import org.apache.maven.plugins.changes.model.FixedIssue; |
| import org.apache.maven.plugins.changes.model.Release; |
| import org.apache.maven.plugins.issues.AbstractIssuesReportGenerator; |
| |
| /** |
| * Generates a changes report. |
| * |
| * @version $Id$ |
| */ |
| public class ChangesReportGenerator extends AbstractIssuesReportGenerator { |
| |
| /** |
| * The token in {@link #issueLinksPerSystem} denoting the base URL for the issue management. |
| */ |
| private static final String URL_TOKEN = "%URL%"; |
| |
| /** |
| * The token in {@link #issueLinksPerSystem} denoting the issue ID. |
| */ |
| private static final String ISSUE_TOKEN = "%ISSUE%"; |
| |
| static final String DEFAULT_ISSUE_SYSTEM_KEY = "default"; |
| |
| private static final String NO_TEAM = "none"; |
| |
| /** |
| * The issue management system to use, for actions that do not specify a system. |
| * |
| * @since 2.4 |
| */ |
| private String system; |
| |
| private String team; |
| |
| private String url; |
| |
| private Map<String, String> issueLinksPerSystem; |
| |
| private boolean addActionDate; |
| |
| private boolean linkToFeed; |
| |
| /** |
| * @since 2.4 |
| */ |
| private boolean escapeHTML; |
| |
| /** |
| * @since 2.4 |
| */ |
| private List<Release> releaseList; |
| |
| public ChangesReportGenerator() { |
| issueLinksPerSystem = new HashMap<>(); |
| } |
| |
| public ChangesReportGenerator(List<Release> releaseList) { |
| this(); |
| this.releaseList = releaseList; |
| } |
| |
| public boolean isEscapeHTML() { |
| return escapeHTML; |
| } |
| |
| public void setEscapeHTML(boolean escapeHTML) { |
| this.escapeHTML = escapeHTML; |
| } |
| |
| public String getSystem() { |
| return system; |
| } |
| |
| public void setSystem(String system) { |
| this.system = system; |
| } |
| |
| public void setTeam(final String team) { |
| this.team = team; |
| } |
| |
| public String getTeam() { |
| return team; |
| } |
| |
| public void setUrl(String url) { |
| this.url = url; |
| } |
| |
| public String getUrl() { |
| return url; |
| } |
| |
| public Map<String, String> getIssueLinksPerSystem() { |
| return issueLinksPerSystem; |
| } |
| |
| public void setIssueLinksPerSystem(Map<String, String> issueLinksPerSystem) { |
| if (this.issueLinksPerSystem != null && issueLinksPerSystem == null) { |
| return; |
| } |
| this.issueLinksPerSystem = issueLinksPerSystem; |
| } |
| |
| public boolean isAddActionDate() { |
| return addActionDate; |
| } |
| |
| public void setAddActionDate(boolean addActionDate) { |
| this.addActionDate = addActionDate; |
| } |
| |
| public boolean isLinkToFeed() { |
| return linkToFeed; |
| } |
| |
| public void setLinkToFeed(boolean generateLinkTofeed) { |
| this.linkToFeed = generateLinkTofeed; |
| } |
| |
| /** |
| * Checks whether links to the issues can be generated for the given system. |
| * |
| * @param system The issue management system |
| * @return <code>true</code> if issue links can be generated, <code>false</code> otherwise. |
| */ |
| public boolean canGenerateIssueLinks(String system) { |
| if (!this.issueLinksPerSystem.containsKey(system)) { |
| return false; |
| } |
| String issueLink = this.issueLinksPerSystem.get(system); |
| |
| // If the issue link entry is blank then no links are possible |
| if (StringUtils.isBlank(issueLink)) { |
| return false; |
| } |
| |
| // If the %URL% token is used then the issue management system URL must be set. |
| if (issueLink.contains(URL_TOKEN) && StringUtils.isBlank(getUrl())) { |
| return false; |
| } |
| return true; |
| } |
| |
| public void doGenerateEmptyReport(ResourceBundle bundle, Sink sink, String message) { |
| sinkBeginReport(sink, bundle); |
| |
| sink.text(message); |
| |
| sinkEndReport(sink); |
| } |
| |
| public void doGenerateReport(ResourceBundle bundle, Sink sink) { |
| sinkBeginReport(sink, bundle); |
| |
| constructReleaseHistory(sink, bundle, releaseList); |
| |
| constructReleases(sink, bundle, releaseList); |
| |
| sinkEndReport(sink); |
| } |
| |
| /** |
| * Constructs table row for specified action with all calculated content (e.g. issue link). |
| * |
| * @param sink Sink |
| * @param bundle Resource bundle |
| * @param action Action to generate content for |
| */ |
| private void constructAction(Sink sink, ResourceBundle bundle, Action action) { |
| sink.tableRow(); |
| |
| sinkShowTypeIcon(sink, action.getType()); |
| |
| sink.tableCell(); |
| |
| String actionDescription = action.getAction(); |
| |
| if (escapeHTML) { |
| sink.text(actionDescription); |
| } else { |
| sink.rawText(actionDescription); |
| } |
| |
| // no null check needed classes from modello return a new ArrayList |
| if (StringUtils.isNotEmpty(action.getIssue()) |
| || (!action.getFixedIssues().isEmpty())) { |
| if (StringUtils.isNotBlank(actionDescription) && !actionDescription.endsWith(".")) { |
| sink.text("."); |
| } |
| sink.text(" " + bundle.getString("report.changes.text.fixes") + " "); |
| |
| // Try to get the issue management system specified in the changes.xml file |
| String system = action.getSystem(); |
| // Try to get the issue management system configured in the POM |
| if (StringUtils.isEmpty(system)) { |
| system = this.system; |
| } |
| // Use the default issue management system |
| if (StringUtils.isEmpty(system)) { |
| system = DEFAULT_ISSUE_SYSTEM_KEY; |
| } |
| if (!canGenerateIssueLinks(system)) { |
| constructIssueText(action.getIssue(), sink, action.getFixedIssues()); |
| } else { |
| constructIssueLink(action.getIssue(), system, sink, action.getFixedIssues()); |
| } |
| sink.text("."); |
| } |
| |
| if (StringUtils.isNotEmpty(action.getDueTo()) || (!action.getDueTos().isEmpty())) { |
| constructDueTo(sink, action, bundle, action.getDueTos()); |
| } |
| |
| sink.tableCell_(); |
| |
| if (NO_TEAM.equals(team)) { |
| sinkCell(sink, action.getDev()); |
| } else { |
| sinkCellLink(sink, action.getDev(), team + "#" + action.getDev()); |
| } |
| |
| if (this.isAddActionDate()) { |
| sinkCell(sink, action.getDate()); |
| } |
| |
| sink.tableRow_(); |
| } |
| |
| /** |
| * Construct a text or link that mention the people that helped with an action. |
| * |
| * @param sink The sink |
| * @param action The action that was done |
| * @param bundle A resource bundle for i18n |
| * @param dueTos Other people that helped with an action |
| */ |
| private void constructDueTo(Sink sink, Action action, ResourceBundle bundle, List<DueTo> dueTos) { |
| |
| // Create a Map with key : dueTo name, value : dueTo email |
| Map<String, String> namesEmailMap = new LinkedHashMap<>(); |
| |
| // Only add the dueTo specified as attributes, if it has either a dueTo or a dueToEmail |
| if (StringUtils.isNotEmpty(action.getDueTo()) || StringUtils.isNotEmpty(action.getDueToEmail())) { |
| namesEmailMap.put(action.getDueTo(), action.getDueToEmail()); |
| } |
| |
| for (DueTo dueTo : dueTos) { |
| namesEmailMap.put(dueTo.getName(), dueTo.getEmail()); |
| } |
| |
| if (namesEmailMap.isEmpty()) { |
| return; |
| } |
| |
| sink.text(" " + bundle.getString("report.changes.text.thanx") + " "); |
| int i = 0; |
| for (String currentDueTo : namesEmailMap.keySet()) { |
| String currentDueToEmail = namesEmailMap.get(currentDueTo); |
| i++; |
| |
| if (StringUtils.isNotEmpty(currentDueToEmail)) { |
| sinkLink(sink, currentDueTo, "mailto:" + currentDueToEmail); |
| } else if (StringUtils.isNotEmpty(currentDueTo)) { |
| sink.text(currentDueTo); |
| } |
| |
| if (i < namesEmailMap.size()) { |
| sink.text(", "); |
| } |
| } |
| |
| sink.text("."); |
| } |
| |
| /** |
| * Construct links to the issues that were solved by an action. |
| * |
| * @param issue The issue specified by attributes |
| * @param system The issue management system |
| * @param sink The sink |
| * @param fixes The List of issues specified as fixes elements |
| */ |
| private void constructIssueLink(String issue, String system, Sink sink, List<FixedIssue> fixes) { |
| if (StringUtils.isNotEmpty(issue)) { |
| sink.link(parseIssueLink(issue, system)); |
| |
| sink.text(issue); |
| |
| sink.link_(); |
| |
| if (!fixes.isEmpty()) { |
| sink.text(", "); |
| } |
| } |
| |
| for (Iterator<FixedIssue> iterator = fixes.iterator(); iterator.hasNext(); ) { |
| FixedIssue fixedIssue = iterator.next(); |
| String currentIssueId = fixedIssue.getIssue(); |
| if (StringUtils.isNotEmpty(currentIssueId)) { |
| sink.link(parseIssueLink(currentIssueId, system)); |
| |
| sink.text(currentIssueId); |
| |
| sink.link_(); |
| } |
| |
| if (iterator.hasNext()) { |
| sink.text(", "); |
| } |
| } |
| } |
| |
| /** |
| * Construct a text that references (but does not link to) the issues that were solved by an action. |
| * |
| * @param issue The issue specified by attributes |
| * @param sink The sink |
| * @param fixes The List of issues specified as fixes elements |
| */ |
| private void constructIssueText(String issue, Sink sink, List<FixedIssue> fixes) { |
| if (StringUtils.isNotEmpty(issue)) { |
| sink.text(issue); |
| |
| if (!fixes.isEmpty()) { |
| sink.text(", "); |
| } |
| } |
| |
| for (Iterator<FixedIssue> iterator = fixes.iterator(); iterator.hasNext(); ) { |
| FixedIssue fixedIssue = iterator.next(); |
| |
| String currentIssueId = fixedIssue.getIssue(); |
| if (StringUtils.isNotEmpty(currentIssueId)) { |
| sink.text(currentIssueId); |
| } |
| |
| if (iterator.hasNext()) { |
| sink.text(", "); |
| } |
| } |
| } |
| |
| private void constructReleaseHistory(Sink sink, ResourceBundle bundle, List<Release> releaseList) { |
| sink.section2(); |
| |
| sink.sectionTitle2(); |
| sink.text(bundle.getString("report.changes.label.releasehistory")); |
| sink.sectionTitle2_(); |
| |
| sink.table(); |
| |
| sink.tableRow(); |
| |
| sinkHeader(sink, bundle.getString("report.issues.label.fixVersion")); |
| |
| sinkHeader(sink, bundle.getString("report.changes.label.releaseDate")); |
| |
| sinkHeader(sink, bundle.getString("report.changes.label.releaseDescription")); |
| |
| sink.tableRow_(); |
| |
| for (Release release : releaseList) { |
| sink.tableRow(); |
| |
| sinkCellLink(sink, release.getVersion(), "#" + HtmlTools.encodeId(release.getVersion())); |
| |
| sinkCell(sink, release.getDateRelease()); |
| |
| sinkCell(sink, release.getDescription()); |
| |
| sink.tableRow_(); |
| } |
| |
| sink.table_(); |
| |
| // MCHANGES-46 |
| if (linkToFeed) { |
| sink.paragraph(); |
| sink.text(bundle.getString("report.changes.text.rssfeed")); |
| sink.nonBreakingSpace(); |
| sink.link("changes.rss"); |
| sinkFigure(sink, "images/rss.png", "rss feed"); |
| sink.link_(); |
| sink.paragraph_(); |
| } |
| |
| sink.section2_(); |
| } |
| |
| /** |
| * Constructs document sections for each of specified releases. |
| * |
| * @param sink Sink |
| * @param bundle Resource bundle |
| * @param releaseList Releases to create content for |
| */ |
| private void constructReleases(Sink sink, ResourceBundle bundle, List<Release> releaseList) { |
| for (Release release : releaseList) { |
| constructRelease(sink, bundle, release); |
| } |
| } |
| |
| /** |
| * Constructs document section for specified release. |
| * |
| * @param sink Sink |
| * @param bundle Resource bundle |
| * @param release Release to create document section for |
| */ |
| private void constructRelease(Sink sink, ResourceBundle bundle, Release release) { |
| sink.section2(); |
| |
| final String date = (release.getDateRelease() == null) ? "" : " \u2013 " + release.getDateRelease(); |
| |
| SinkEventAttributes attrs = new SinkEventAttributeSet(); |
| attrs.addAttribute(SinkEventAttributes.ID, HtmlTools.encodeId(release.getVersion())); |
| sink.sectionTitle(Sink.SECTION_LEVEL_2, attrs); |
| sink.text(bundle.getString("report.changes.label.release") + " " + release.getVersion() + date); |
| sink.sectionTitle_(Sink.SECTION_LEVEL_2); |
| |
| if (isReleaseEmpty(release)) { |
| sink.paragraph(); |
| sink.text(bundle.getString("report.changes.text.no.changes")); |
| sink.paragraph_(); |
| } else { |
| sink.table(); |
| |
| sink.tableRow(); |
| sinkHeader(sink, bundle.getString("report.issues.label.type")); |
| sinkHeader(sink, bundle.getString("report.issues.label.summary")); |
| sinkHeader(sink, bundle.getString("report.issues.label.assignee")); |
| if (this.isAddActionDate()) { |
| sinkHeader(sink, bundle.getString("report.issues.label.updated")); |
| } |
| sink.tableRow_(); |
| |
| for (Action action : release.getActions()) { |
| constructAction(sink, bundle, action); |
| } |
| |
| for (Object o : release.getComponents()) { |
| Component component = (Component) o; |
| constructComponent(sink, bundle, component); |
| } |
| |
| sink.table_(); |
| } |
| |
| sink.section2_(); |
| } |
| |
| /** |
| * Constructs table rows for specified release component. It will create header row for component name and action |
| * rows for all component issues. |
| * |
| * @param sink Sink |
| * @param bundle Resource bundle |
| * @param component Release component to generate content for. |
| */ |
| private void constructComponent(Sink sink, ResourceBundle bundle, Component component) { |
| if (!component.getActions().isEmpty()) { |
| sink.tableRow(); |
| |
| sink.tableHeaderCell(); |
| sink.tableHeaderCell_(); |
| |
| sink.tableHeaderCell(); |
| sink.text(component.getName()); |
| sink.tableHeaderCell_(); |
| |
| sink.tableHeaderCell(); |
| sink.tableHeaderCell_(); |
| |
| if (isAddActionDate()) { |
| sink.tableHeaderCell(); |
| sink.tableHeaderCell_(); |
| } |
| |
| sink.tableRow_(); |
| |
| for (Action action : component.getActions()) { |
| constructAction(sink, bundle, action); |
| } |
| } |
| } |
| |
| /** |
| * Checks if specified release contains own issues or issues inside the child components. |
| * |
| * @param release Release to check |
| * @return <code>true</code> if release doesn't contain any issues, <code>false</code> otherwise |
| */ |
| private boolean isReleaseEmpty(Release release) { |
| if (!release.getActions().isEmpty()) { |
| return false; |
| } |
| |
| for (Object o : release.getComponents()) { |
| Component component = (Component) o; |
| if (!component.getActions().isEmpty()) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Replace tokens in the issue link template with the real values. |
| * |
| * @param issue The issue identifier |
| * @param system The issue management system |
| * @return An interpolated issue link |
| */ |
| private String parseIssueLink(String issue, String system) { |
| String parseLink; |
| String issueLink = this.issueLinksPerSystem.get(system); |
| parseLink = issueLink.replaceFirst(ISSUE_TOKEN, issue); |
| if (parseLink.contains(URL_TOKEN)) { |
| String url = this.url.substring(0, this.url.lastIndexOf("/")); |
| parseLink = parseLink.replaceFirst(URL_TOKEN, url); |
| } |
| |
| return parseLink; |
| } |
| } |