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      private boolean escapeText;
77  
78      public ChangesReportRenderer(Sink sink, ResourceBundle bundleName, ChangesXML changesXML) {
79          super(sink, bundleName);
80          this.issueLinksPerSystem = new HashMap<>();
81          this.changesXML = changesXML;
82      }
83  
84      public void setSystem(String system) {
85          this.system = system;
86      }
87  
88      public void setTeam(final String team) {
89          this.team = team;
90      }
91  
92      public void setUrl(String url) {
93          this.url = url;
94      }
95  
96      public void setIssueLinksPerSystem(Map<String, String> issueLinksPerSystem) {
97          if (this.issueLinksPerSystem != null && issueLinksPerSystem == null) {
98              return;
99          }
100         this.issueLinksPerSystem = issueLinksPerSystem;
101     }
102 
103     public void setAddActionDate(boolean addActionDate) {
104         this.addActionDate = addActionDate;
105     }
106 
107     public void setLinkToFeed(boolean generateLinkTofeed) {
108         this.linkToFeed = generateLinkTofeed;
109     }
110 
111     public void setEscapeText(boolean escapeText) {
112         this.escapeText = escapeText;
113     }
114 
115     /**
116      * Checks whether links to the issues can be generated for the given system.
117      *
118      * @param system The issue management system
119      * @return <code>true</code> if issue links can be generated, <code>false</code> otherwise.
120      */
121     private boolean canGenerateIssueLinks(String system) {
122         if (!this.issueLinksPerSystem.containsKey(system)) {
123             return false;
124         }
125         String issueLink = this.issueLinksPerSystem.get(system);
126 
127         // If the issue link entry is blank then no links are possible
128         if (StringUtils.isBlank(issueLink)) {
129             return false;
130         }
131 
132         // If the %URL% token is used then the issue management system URL must be set.
133         if (issueLink.contains(URL_TOKEN) && StringUtils.isBlank(url)) {
134             return false;
135         }
136         return true;
137     }
138 
139     @Override
140     protected void renderBody() {
141         constructReleaseHistory();
142         constructReleases();
143     }
144 
145     @Override
146     public String getTitle() {
147         String title = changesXML.getTitle();
148         if (title == null) {
149             title = bundle.getString("report.issues.header");
150         }
151         return title;
152     }
153 
154     /**
155      * Constructs table row for specified action with all calculated content (e.g. issue link).
156      *
157      * @param action Action to generate content for
158      */
159     private void constructAction(Action action) {
160         sink.tableRow();
161 
162         sinkShowTypeIcon(action.getType());
163 
164         sink.tableCell();
165 
166         String actionDescription = action.getAction();
167 
168         if (escapeText || StringUtils.isEmpty(actionDescription)) {
169             text(actionDescription);
170         } else {
171             sink.rawText(actionDescription);
172         }
173 
174         // no null check needed classes from modello return a new ArrayList
175         if (StringUtils.isNotEmpty(action.getIssue())
176                 || (!action.getFixedIssues().isEmpty())) {
177             if (StringUtils.isNotBlank(actionDescription) && !actionDescription.endsWith(".")) {
178                 text(".");
179             }
180             text(" " + bundle.getString("report.changes.text.fixes") + " ");
181 
182             // Try to get the issue management system specified in the changes.xml file
183             String system = action.getSystem();
184             // Try to get the issue management system configured in the POM
185             if (StringUtils.isEmpty(system)) {
186                 system = this.system;
187             }
188             // Use the default issue management system
189             if (StringUtils.isEmpty(system)) {
190                 system = DEFAULT_ISSUE_SYSTEM_KEY;
191             }
192             if (!canGenerateIssueLinks(system)) {
193                 constructIssueText(action.getIssue(), action.getFixedIssues());
194             } else {
195                 constructIssueLink(action.getIssue(), system, action.getFixedIssues());
196             }
197             text(".");
198         }
199 
200         if (!action.getDueTos().isEmpty()) {
201             constructDueTo(action);
202         }
203 
204         sink.tableCell_();
205 
206         if (NO_TEAM.equals(team) || action.getDev() == null || action.getDev().isEmpty()) {
207             sinkCell(action.getDev());
208         } else {
209             sinkCellLink(action.getDev(), team + "#" + action.getDev());
210         }
211 
212         if (addActionDate) {
213             sinkCell(action.getDate());
214         }
215 
216         sink.tableRow_();
217     }
218 
219     /**
220      * Construct a text or link that mention the people that helped with an action.
221      *
222      * @param action The action that was done
223      */
224     private void constructDueTo(Action action) {
225 
226         text(" " + bundle.getString("report.changes.text.thanx") + " ");
227         int i = 0;
228         for (DueTo dueTo : action.getDueTos()) {
229             i++;
230 
231             if (StringUtils.isNotEmpty(dueTo.getEmail())) {
232                 String text = dueTo.getName();
233                 link("mailto:" + dueTo.getEmail(), text);
234             } else {
235                 text(dueTo.getName());
236             }
237 
238             if (i < action.getDueTos().size()) {
239                 text(", ");
240             }
241         }
242 
243         text(".");
244     }
245 
246     /**
247      * Construct links to the issues that were solved by an action.
248      *
249      * @param issue The issue specified by attributes
250      * @param system The issue management system
251      * @param fixes The List of issues specified as fixes elements
252      */
253     private void constructIssueLink(String issue, String system, List<String> fixes) {
254         if (StringUtils.isNotEmpty(issue)) {
255             link(parseIssueLink(issue, system), issue);
256             if (!fixes.isEmpty()) {
257                 text(", ");
258             }
259         }
260 
261         for (Iterator<String> iterator = fixes.iterator(); iterator.hasNext(); ) {
262             String currentIssueId = iterator.next();
263             if (StringUtils.isNotEmpty(currentIssueId)) {
264                 link(parseIssueLink(currentIssueId, system), currentIssueId);
265             }
266 
267             if (iterator.hasNext()) {
268                 text(", ");
269             }
270         }
271     }
272 
273     /**
274      * Construct a text that references (but does not link to) the issues that were solved by an action.
275      *
276      * @param issue The issue specified by attributes
277      * @param fixes The List of issues specified as fixes elements
278      */
279     private void constructIssueText(String issue, List<String> fixes) {
280         if (StringUtils.isNotEmpty(issue)) {
281             text(issue);
282 
283             if (!fixes.isEmpty()) {
284                 text(", ");
285             }
286         }
287 
288         for (Iterator<String> iterator = fixes.iterator(); iterator.hasNext(); ) {
289             String currentIssueId = iterator.next();
290             if (StringUtils.isNotEmpty(currentIssueId)) {
291                 text(currentIssueId);
292             }
293 
294             if (iterator.hasNext()) {
295                 text(", ");
296             }
297         }
298     }
299 
300     private void constructReleaseHistory() {
301         startSection(bundle.getString("report.changes.label.releasehistory"));
302 
303         startTable();
304 
305         tableHeader(new String[] {
306             bundle.getString("report.issues.label.fixVersion"),
307             bundle.getString("report.changes.label.releaseDate"),
308             bundle.getString("report.changes.label.releaseDescription")
309         });
310 
311         for (Release release : changesXML.getReleaseList()) {
312             sink.tableRow();
313             sinkCellLink(release.getVersion(), "#" + DoxiaUtils.encodeId(release.getVersion()));
314             sinkCell(release.getDateRelease());
315             sinkCell(release.getDescription());
316             sink.tableRow_();
317         }
318 
319         endTable();
320 
321         // MCHANGES-46
322         if (linkToFeed) {
323             sink.paragraph();
324             text(bundle.getString("report.changes.text.rssfeed"));
325             sink.nonBreakingSpace();
326             sink.link("changes.rss");
327             sinkFigure("images/rss.png", "rss feed");
328             sink.link_();
329             sink.paragraph_();
330         }
331 
332         endSection();
333     }
334 
335     /**
336      * Constructs document sections for each of specified releases.
337      */
338     private void constructReleases() {
339         for (Release release : changesXML.getReleaseList()) {
340             constructRelease(release);
341         }
342     }
343 
344     /**
345      * Constructs document section for specified release.
346      *
347      * @param release Release to create document section for
348      */
349     private void constructRelease(Release release) {
350 
351         final String date = (release.getDateRelease() == null) ? "" : " \u2013 " + release.getDateRelease();
352 
353         startSection(
354                 bundle.getString("report.changes.label.release") + " " + release.getVersion() + date,
355                 DoxiaUtils.encodeId(release.getVersion()));
356 
357         if (isReleaseEmpty(release)) {
358             sink.paragraph();
359             text(bundle.getString("report.changes.text.no.changes"));
360             sink.paragraph_();
361         } else {
362             startTable();
363 
364             sink.tableRow();
365             tableHeaderCell(bundle.getString("report.issues.label.type"));
366             tableHeaderCell(bundle.getString("report.issues.label.summary"));
367             tableHeaderCell(bundle.getString("report.issues.label.assignee"));
368             if (addActionDate) {
369                 tableHeaderCell(bundle.getString("report.issues.label.updated"));
370             }
371             sink.tableRow_();
372 
373             for (Action action : release.getActions()) {
374                 constructAction(action);
375             }
376 
377             for (Component component : release.getComponents()) {
378                 constructComponent(component);
379             }
380 
381             endTable();
382         }
383 
384         endSection();
385     }
386 
387     /**
388      * Constructs table rows for specified release component. It will create header row for component name and action
389      * rows for all component issues.
390      *
391      * @param component Release component to generate content for.
392      */
393     private void constructComponent(Component component) {
394         if (!component.getActions().isEmpty()) {
395             sink.tableRow();
396 
397             sink.tableHeaderCell();
398             sink.tableHeaderCell_();
399 
400             sink.tableHeaderCell();
401             text(component.getName());
402             sink.tableHeaderCell_();
403 
404             sink.tableHeaderCell();
405             sink.tableHeaderCell_();
406 
407             if (addActionDate) {
408                 sink.tableHeaderCell();
409                 sink.tableHeaderCell_();
410             }
411 
412             sink.tableRow_();
413 
414             for (Action action : component.getActions()) {
415                 constructAction(action);
416             }
417         }
418     }
419 
420     /**
421      * Checks if specified release contains own issues or issues inside the child components.
422      *
423      * @param release Release to check
424      * @return <code>true</code> if release doesn't contain any issues, <code>false</code> otherwise
425      */
426     private boolean isReleaseEmpty(Release release) {
427         if (!release.getActions().isEmpty()) {
428             return false;
429         }
430 
431         for (Component component : release.getComponents()) {
432             if (!component.getActions().isEmpty()) {
433                 return false;
434             }
435         }
436 
437         return true;
438     }
439 
440     /**
441      * Replace tokens in the issue link template with the real values.
442      *
443      * @param issue The issue identifier
444      * @param system The issue management system
445      * @return An interpolated issue link
446      */
447     private String parseIssueLink(String issue, String system) {
448         String parseLink;
449         String issueLink = this.issueLinksPerSystem.get(system);
450         parseLink = issueLink.replaceFirst(ISSUE_TOKEN, issue);
451         if (parseLink.contains(URL_TOKEN)) {
452             String url = this.url.substring(0, this.url.lastIndexOf("/"));
453             parseLink = parseLink.replaceFirst(URL_TOKEN, url);
454         }
455 
456         return parseLink;
457     }
458 }