blob: a1ff1dfba9bb3915f76c6752cc79e9f86c257c68 [file] [log] [blame]
/*
* 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;
}
}