View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.changes;
20  
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.ResourceBundle;
26  
27  import org.apache.commons.lang3.StringUtils;
28  import org.apache.maven.doxia.sink.Sink;
29  import org.apache.maven.doxia.util.DoxiaUtils;
30  import org.apache.maven.plugins.changes.issues.AbstractIssuesReportRenderer;
31  import org.apache.maven.plugins.changes.model.Action;
32  import org.apache.maven.plugins.changes.model.Component;
33  import org.apache.maven.plugins.changes.model.DueTo;
34  import org.apache.maven.plugins.changes.model.Release;
35  
36  /**
37   * Generates a changes report.
38   *
39   * @version $Id$
40   */
41  public class ChangesReportRenderer extends AbstractIssuesReportRenderer {
42  
43      /**
44       * The token in {@link #issueLinksPerSystem} denoting the base URL for the issue management.
45       */
46      private static final String URL_TOKEN = "%URL%";
47  
48      /**
49       * The token in {@link #issueLinksPerSystem} denoting the issue ID.
50       */
51      private static final String ISSUE_TOKEN = "%ISSUE%";
52  
53      static final String DEFAULT_ISSUE_SYSTEM_KEY = "default";
54  
55      private static final String NO_TEAM = "none";
56  
57      private final ChangesXML changesXML;
58  
59      /**
60       * The issue management system to use, for actions that do not specify a system.
61       *
62       * @since 2.4
63       */
64      private String system;
65  
66      private String team;
67  
68      private String url;
69  
70      private Map<String, String> issueLinksPerSystem;
71  
72      private boolean addActionDate;
73  
74      private boolean linkToFeed;
75  
76      public ChangesReportRenderer(Sink sink, ResourceBundle bundleName, ChangesXML changesXML) {
77          super(sink, bundleName);
78          this.issueLinksPerSystem = new HashMap<>();
79          this.changesXML = changesXML;
80      }
81  
82      public void setSystem(String system) {
83          this.system = system;
84      }
85  
86      public void setTeam(final String team) {
87          this.team = team;
88      }
89  
90      public void setUrl(String url) {
91          this.url = url;
92      }
93  
94      public void setIssueLinksPerSystem(Map<String, String> issueLinksPerSystem) {
95          if (this.issueLinksPerSystem != null && issueLinksPerSystem == null) {
96              return;
97          }
98          this.issueLinksPerSystem = issueLinksPerSystem;
99      }
100 
101     public void setAddActionDate(boolean addActionDate) {
102         this.addActionDate = addActionDate;
103     }
104 
105     public void setLinkToFeed(boolean generateLinkTofeed) {
106         this.linkToFeed = generateLinkTofeed;
107     }
108 
109     /**
110      * Checks whether links to the issues can be generated for the given system.
111      *
112      * @param system The issue management system
113      * @return <code>true</code> if issue links can be generated, <code>false</code> otherwise.
114      */
115     private boolean canGenerateIssueLinks(String system) {
116         if (!this.issueLinksPerSystem.containsKey(system)) {
117             return false;
118         }
119         String issueLink = this.issueLinksPerSystem.get(system);
120 
121         // If the issue link entry is blank then no links are possible
122         if (StringUtils.isBlank(issueLink)) {
123             return false;
124         }
125 
126         // If the %URL% token is used then the issue management system URL must be set.
127         if (issueLink.contains(URL_TOKEN) && StringUtils.isBlank(url)) {
128             return false;
129         }
130         return true;
131     }
132 
133     @Override
134     protected void renderBody() {
135         constructReleaseHistory();
136         constructReleases();
137     }
138 
139     @Override
140     public String getTitle() {
141         String title = changesXML.getTitle();
142         if (title == null) {
143             title = bundle.getString("report.issues.header");
144         }
145         return title;
146     }
147 
148     /**
149      * Constructs table row for specified action with all calculated content (e.g. issue link).
150      *
151      * @param action Action to generate content for
152      */
153     private void constructAction(Action action) {
154         sink.tableRow();
155 
156         sinkShowTypeIcon(action.getType());
157 
158         sink.tableCell();
159 
160         String actionDescription = action.getAction();
161 
162         text(actionDescription);
163 
164         // no null check needed classes from modello return a new ArrayList
165         if (StringUtils.isNotEmpty(action.getIssue())
166                 || (!action.getFixedIssues().isEmpty())) {
167             if (StringUtils.isNotBlank(actionDescription) && !actionDescription.endsWith(".")) {
168                 text(".");
169             }
170             text(" " + bundle.getString("report.changes.text.fixes") + " ");
171 
172             // Try to get the issue management system specified in the changes.xml file
173             String system = action.getSystem();
174             // Try to get the issue management system configured in the POM
175             if (StringUtils.isEmpty(system)) {
176                 system = this.system;
177             }
178             // Use the default issue management system
179             if (StringUtils.isEmpty(system)) {
180                 system = DEFAULT_ISSUE_SYSTEM_KEY;
181             }
182             if (!canGenerateIssueLinks(system)) {
183                 constructIssueText(action.getIssue(), action.getFixedIssues());
184             } else {
185                 constructIssueLink(action.getIssue(), system, action.getFixedIssues());
186             }
187             text(".");
188         }
189 
190         if (!action.getDueTos().isEmpty()) {
191             constructDueTo(action);
192         }
193 
194         sink.tableCell_();
195 
196         if (NO_TEAM.equals(team)) {
197             sinkCell(action.getDev());
198         } else {
199             sinkCellLink(action.getDev(), team + "#" + action.getDev());
200         }
201 
202         if (addActionDate) {
203             sinkCell(action.getDate());
204         }
205 
206         sink.tableRow_();
207     }
208 
209     /**
210      * Construct a text or link that mention the people that helped with an action.
211      *
212      * @param action The action that was done
213      */
214     private void constructDueTo(Action action) {
215 
216         text(" " + bundle.getString("report.changes.text.thanx") + " ");
217         int i = 0;
218         for (DueTo dueTo : action.getDueTos()) {
219             i++;
220 
221             if (StringUtils.isNotEmpty(dueTo.getEmail())) {
222                 String text = dueTo.getName();
223                 link("mailto:" + dueTo.getEmail(), text);
224             } else {
225                 text(dueTo.getName());
226             }
227 
228             if (i < action.getDueTos().size()) {
229                 text(", ");
230             }
231         }
232 
233         text(".");
234     }
235 
236     /**
237      * Construct links to the issues that were solved by an action.
238      *
239      * @param issue The issue specified by attributes
240      * @param system The issue management system
241      * @param fixes The List of issues specified as fixes elements
242      */
243     private void constructIssueLink(String issue, String system, List<String> fixes) {
244         if (StringUtils.isNotEmpty(issue)) {
245             link(parseIssueLink(issue, system), issue);
246             if (!fixes.isEmpty()) {
247                 text(", ");
248             }
249         }
250 
251         for (Iterator<String> iterator = fixes.iterator(); iterator.hasNext(); ) {
252             String currentIssueId = iterator.next();
253             if (StringUtils.isNotEmpty(currentIssueId)) {
254                 link(parseIssueLink(currentIssueId, system), currentIssueId);
255             }
256 
257             if (iterator.hasNext()) {
258                 text(", ");
259             }
260         }
261     }
262 
263     /**
264      * Construct a text that references (but does not link to) the issues that were solved by an action.
265      *
266      * @param issue The issue specified by attributes
267      * @param fixes The List of issues specified as fixes elements
268      */
269     private void constructIssueText(String issue, List<String> fixes) {
270         if (StringUtils.isNotEmpty(issue)) {
271             text(issue);
272 
273             if (!fixes.isEmpty()) {
274                 text(", ");
275             }
276         }
277 
278         for (Iterator<String> iterator = fixes.iterator(); iterator.hasNext(); ) {
279             String currentIssueId = iterator.next();
280             if (StringUtils.isNotEmpty(currentIssueId)) {
281                 text(currentIssueId);
282             }
283 
284             if (iterator.hasNext()) {
285                 text(", ");
286             }
287         }
288     }
289 
290     private void constructReleaseHistory() {
291         startSection(bundle.getString("report.changes.label.releasehistory"));
292 
293         startTable();
294 
295         tableHeader(new String[] {
296             bundle.getString("report.issues.label.fixVersion"),
297             bundle.getString("report.changes.label.releaseDate"),
298             bundle.getString("report.changes.label.releaseDescription")
299         });
300 
301         for (Release release : changesXML.getReleaseList()) {
302             sink.tableRow();
303             sinkCellLink(release.getVersion(), "#" + DoxiaUtils.encodeId(release.getVersion()));
304             sinkCell(release.getDateRelease());
305             sinkCell(release.getDescription());
306             sink.tableRow_();
307         }
308 
309         endTable();
310 
311         // MCHANGES-46
312         if (linkToFeed) {
313             sink.paragraph();
314             text(bundle.getString("report.changes.text.rssfeed"));
315             sink.nonBreakingSpace();
316             sink.link("changes.rss");
317             sinkFigure("images/rss.png", "rss feed");
318             sink.link_();
319             sink.paragraph_();
320         }
321 
322         endSection();
323     }
324 
325     /**
326      * Constructs document sections for each of specified releases.
327      */
328     private void constructReleases() {
329         for (Release release : changesXML.getReleaseList()) {
330             constructRelease(release);
331         }
332     }
333 
334     /**
335      * Constructs document section for specified release.
336      *
337      * @param release Release to create document section for
338      */
339     private void constructRelease(Release release) {
340 
341         final String date = (release.getDateRelease() == null) ? "" : " \u2013 " + release.getDateRelease();
342 
343         startSection(
344                 bundle.getString("report.changes.label.release") + " " + release.getVersion() + date,
345                 DoxiaUtils.encodeId(release.getVersion()));
346 
347         if (isReleaseEmpty(release)) {
348             sink.paragraph();
349             text(bundle.getString("report.changes.text.no.changes"));
350             sink.paragraph_();
351         } else {
352             startTable();
353 
354             sink.tableRow();
355             tableHeaderCell(bundle.getString("report.issues.label.type"));
356             tableHeaderCell(bundle.getString("report.issues.label.summary"));
357             tableHeaderCell(bundle.getString("report.issues.label.assignee"));
358             if (addActionDate) {
359                 tableHeaderCell(bundle.getString("report.issues.label.updated"));
360             }
361             sink.tableRow_();
362 
363             for (Action action : release.getActions()) {
364                 constructAction(action);
365             }
366 
367             for (Component component : release.getComponents()) {
368                 constructComponent(component);
369             }
370 
371             endTable();
372         }
373 
374         endSection();
375     }
376 
377     /**
378      * Constructs table rows for specified release component. It will create header row for component name and action
379      * rows for all component issues.
380      *
381      * @param component Release component to generate content for.
382      */
383     private void constructComponent(Component component) {
384         if (!component.getActions().isEmpty()) {
385             sink.tableRow();
386 
387             sink.tableHeaderCell();
388             sink.tableHeaderCell_();
389 
390             sink.tableHeaderCell();
391             text(component.getName());
392             sink.tableHeaderCell_();
393 
394             sink.tableHeaderCell();
395             sink.tableHeaderCell_();
396 
397             if (addActionDate) {
398                 sink.tableHeaderCell();
399                 sink.tableHeaderCell_();
400             }
401 
402             sink.tableRow_();
403 
404             for (Action action : component.getActions()) {
405                 constructAction(action);
406             }
407         }
408     }
409 
410     /**
411      * Checks if specified release contains own issues or issues inside the child components.
412      *
413      * @param release Release to check
414      * @return <code>true</code> if release doesn't contain any issues, <code>false</code> otherwise
415      */
416     private boolean isReleaseEmpty(Release release) {
417         if (!release.getActions().isEmpty()) {
418             return false;
419         }
420 
421         for (Component component : release.getComponents()) {
422             if (!component.getActions().isEmpty()) {
423                 return false;
424             }
425         }
426 
427         return true;
428     }
429 
430     /**
431      * Replace tokens in the issue link template with the real values.
432      *
433      * @param issue The issue identifier
434      * @param system The issue management system
435      * @return An interpolated issue link
436      */
437     private String parseIssueLink(String issue, String system) {
438         String parseLink;
439         String issueLink = this.issueLinksPerSystem.get(system);
440         parseLink = issueLink.replaceFirst(ISSUE_TOKEN, issue);
441         if (parseLink.contains(URL_TOKEN)) {
442             String url = this.url.substring(0, this.url.lastIndexOf("/"));
443             parseLink = parseLink.replaceFirst(URL_TOKEN, url);
444         }
445 
446         return parseLink;
447     }
448 }