View Javadoc
1   package org.apache.maven.plugin.changes;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.ResourceBundle;
28  
29  import org.apache.commons.lang.StringUtils;
30  
31  import org.apache.maven.doxia.sink.Sink;
32  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
33  import org.apache.maven.doxia.sink.SinkEventAttributes;
34  import org.apache.maven.doxia.util.HtmlTools;
35  import org.apache.maven.plugin.issues.AbstractIssuesReportGenerator;
36  import org.apache.maven.plugins.changes.model.Action;
37  import org.apache.maven.plugins.changes.model.Component;
38  import org.apache.maven.plugins.changes.model.DueTo;
39  import org.apache.maven.plugins.changes.model.FixedIssue;
40  import org.apache.maven.plugins.changes.model.Release;
41  
42  /**
43   * Generates a changes report.
44   *
45   * @version $Id: ChangesReportGenerator.java 1737092 2016-03-30 10:29:33Z michaelo $
46   */
47  public class ChangesReportGenerator
48      extends AbstractIssuesReportGenerator
49  {
50  
51      /**
52       * The token in {@link #issueLinksPerSystem} denoting the base URL for the issue management.
53       */
54      private static final String URL_TOKEN = "%URL%";
55  
56      /**
57       * The token in {@link #issueLinksPerSystem} denoting the issue ID.
58       */
59      private static final String ISSUE_TOKEN = "%ISSUE%";
60  
61      static final String DEFAULT_ISSUE_SYSTEM_KEY = "default";
62  
63      private static final String NO_TEAMLIST = "none";
64  
65      /**
66       * The issue management system to use, for actions that do not specify a system.
67       *
68       * @since 2.4
69       */
70      private String system;
71  
72      private String teamlist;
73  
74      private String url;
75  
76      private Map<String, String> issueLinksPerSystem;
77  
78      private boolean addActionDate;
79  
80      private boolean linkToFeed;
81  
82      /**
83       * @since 2.4
84       */
85      private boolean escapeHTML;
86  
87      /**
88       * @since 2.4
89       */
90      private List<Release> releaseList;
91  
92      public ChangesReportGenerator()
93      {
94          issueLinksPerSystem = new HashMap<String, String>();
95      }
96  
97      public ChangesReportGenerator( List<Release> releaseList )
98      {
99          this();
100         this.releaseList = releaseList;
101     }
102 
103     /**
104      * @since 2.4
105      */
106     public boolean isEscapeHTML()
107     {
108         return escapeHTML;
109     }
110 
111     /**
112      * @since 2.4
113      */
114     public void setEscapeHTML( boolean escapeHTML )
115     {
116         this.escapeHTML = escapeHTML;
117     }
118 
119     /**
120      * @since 2.4
121      */
122     public String getSystem()
123     {
124         return system;
125     }
126 
127     /**
128      * @since 2.4
129      */
130     public void setSystem( String system )
131     {
132         this.system = system;
133     }
134 
135     public void setTeamlist( final String teamlist )
136     {
137         this.teamlist = teamlist;
138     }
139 
140     public String getTeamlist()
141     {
142         return teamlist;
143     }
144 
145     public void setUrl( String url )
146     {
147         this.url = url;
148     }
149 
150     public String getUrl()
151     {
152         return url;
153     }
154 
155     public Map<String, String> getIssueLinksPerSystem()
156     {
157         return issueLinksPerSystem;
158     }
159 
160     public void setIssueLinksPerSystem( Map<String, String> issueLinksPerSystem )
161     {
162         if ( this.issueLinksPerSystem != null && issueLinksPerSystem == null )
163         {
164             return;
165         }
166         this.issueLinksPerSystem = issueLinksPerSystem;
167     }
168 
169     public boolean isAddActionDate()
170     {
171         return addActionDate;
172     }
173 
174     public void setAddActionDate( boolean addActionDate )
175     {
176         this.addActionDate = addActionDate;
177     }
178 
179     public boolean isLinkToFeed()
180     {
181         return linkToFeed;
182     }
183 
184     public void setLinkToFeed( boolean generateLinkTofeed )
185     {
186         this.linkToFeed = generateLinkTofeed;
187     }
188 
189     /**
190      * Checks whether links to the issues can be generated for the given system.
191      *
192      * @param system The issue management system
193      * @return <code>true</code> if issue links can be generated, <code>false</code> otherwise.
194      */
195     public boolean canGenerateIssueLinks( String system )
196     {
197         if ( !this.issueLinksPerSystem.containsKey( system ) )
198         {
199             return false;
200         }
201         String issueLink = this.issueLinksPerSystem.get( system );
202 
203         // If the issue link entry is blank then no links are possible
204         if ( StringUtils.isBlank( issueLink ) )
205         {
206             return false;
207         }
208 
209         // If the %URL% token is used then the issue management system URL must be set.
210         if ( issueLink.contains( URL_TOKEN ) && StringUtils.isBlank( getUrl() ) )
211         {
212             return false;
213         }
214         return true;
215     }
216 
217     public void doGenerateEmptyReport( ResourceBundle bundle, Sink sink, String message )
218     {
219         sinkBeginReport( sink, bundle );
220 
221         sink.text( message );
222 
223         sinkEndReport( sink );
224     }
225 
226     public void doGenerateReport( ResourceBundle bundle, Sink sink )
227     {
228         sinkBeginReport( sink, bundle );
229 
230         constructReleaseHistory( sink, bundle, releaseList );
231 
232         constructReleases( sink, bundle, releaseList );
233 
234         sinkEndReport( sink );
235     }
236 
237     /**
238      * Constructs table row for specified action with all calculated content (e.g. issue link).
239      *
240      * @param sink Sink
241      * @param bundle Resource bundle
242      * @param action Action to generate content for
243      */
244     private void constructAction( Sink sink, ResourceBundle bundle, Action action )
245     {
246         sink.tableRow();
247 
248         sinkShowTypeIcon( sink, action.getType() );
249 
250         sink.tableCell();
251 
252         if ( escapeHTML )
253         {
254             sink.text( action.getAction() );
255         }
256         else
257         {
258             sink.rawText( action.getAction() );
259         }
260 
261         // no null check needed classes from modello return a new ArrayList
262         if ( StringUtils.isNotEmpty( action.getIssue() ) || ( !action.getFixedIssues().isEmpty() ) )
263         {
264             sink.text( " " + bundle.getString( "report.changes.text.fixes" ) + " " );
265 
266             // Try to get the issue management system specified in the changes.xml file
267             String system = action.getSystem();
268             // Try to get the issue management system configured in the POM
269             if ( StringUtils.isEmpty( system ) )
270             {
271                 system = this.system;
272             }
273             // Use the default issue management system
274             if ( StringUtils.isEmpty( system ) )
275             {
276                 system = DEFAULT_ISSUE_SYSTEM_KEY;
277             }
278             if ( !canGenerateIssueLinks( system ) )
279             {
280                 constructIssueText( action.getIssue(), sink, action.getFixedIssues() );
281             }
282             else
283             {
284                 constructIssueLink( action.getIssue(), system, sink, action.getFixedIssues() );
285             }
286             sink.text( "." );
287         }
288 
289         if ( StringUtils.isNotEmpty( action.getDueTo() ) || ( !action.getDueTos().isEmpty() ) )
290         {
291             constructDueTo( sink, action, bundle, action.getDueTos() );
292         }
293 
294         sink.tableCell_();
295 
296         if ( NO_TEAMLIST.equals( teamlist ) )
297         {
298             sinkCell( sink, action.getDev() );
299         }
300         else
301         {
302             sinkCellLink( sink, action.getDev(), teamlist + "#" + action.getDev() );
303         }
304 
305         if ( this.isAddActionDate() )
306         {
307             sinkCell( sink, action.getDate() );
308         }
309 
310         sink.tableRow_();
311     }
312 
313     /**
314      * Construct a text or link that mention the people that helped with an action.
315      *
316      * @param sink The sink
317      * @param action The action that was done
318      * @param bundle A resource bundle for i18n
319      * @param dueTos Other people that helped with an action
320      */
321     private void constructDueTo( Sink sink, Action action, ResourceBundle bundle, List<DueTo> dueTos )
322     {
323 
324         // Create a Map with key : dueTo name, value : dueTo email
325         Map<String, String> namesEmailMap = new LinkedHashMap<String, String>();
326 
327         // Only add the dueTo specified as attributes, if it has either a dueTo or a dueToEmail
328         if ( StringUtils.isNotEmpty( action.getDueTo() ) || StringUtils.isNotEmpty( action.getDueToEmail() ) )
329         {
330             namesEmailMap.put( action.getDueTo(), action.getDueToEmail() );
331         }
332 
333         for ( DueTo dueTo : dueTos )
334         {
335             namesEmailMap.put( dueTo.getName(), dueTo.getEmail() );
336         }
337 
338         if ( namesEmailMap.isEmpty() )
339         {
340             return;
341         }
342 
343         sink.text( " " + bundle.getString( "report.changes.text.thanx" ) + " " );
344         int i = 0;
345         for ( String currentDueTo : namesEmailMap.keySet() )
346         {
347             String currentDueToEmail = namesEmailMap.get( currentDueTo );
348             i++;
349 
350             if ( StringUtils.isNotEmpty( currentDueToEmail ) )
351             {
352                 sinkLink( sink, currentDueTo, "mailto:" + currentDueToEmail );
353             }
354             else if ( StringUtils.isNotEmpty( currentDueTo ) )
355             {
356                 sink.text( currentDueTo );
357             }
358 
359             if ( i < namesEmailMap.size() )
360             {
361                 sink.text( ", " );
362             }
363         }
364 
365         sink.text( "." );
366     }
367 
368     /**
369      * Construct links to the issues that were solved by an action.
370      *
371      * @param issue The issue specified by attributes
372      * @param system The issue management system
373      * @param sink The sink
374      * @param fixes The List of issues specified as fixes elements
375      */
376     private void constructIssueLink( String issue, String system, Sink sink, List<FixedIssue> fixes )
377     {
378         if ( StringUtils.isNotEmpty( issue ) )
379         {
380             sink.link( parseIssueLink( issue, system ) );
381 
382             sink.text( issue );
383 
384             sink.link_();
385 
386             if ( !fixes.isEmpty() )
387             {
388                 sink.text( ", " );
389             }
390         }
391 
392         for ( Iterator<FixedIssue> iterator = fixes.iterator(); iterator.hasNext(); )
393         {
394             FixedIssue fixedIssue = iterator.next();
395             String currentIssueId = fixedIssue.getIssue();
396             if ( StringUtils.isNotEmpty( currentIssueId ) )
397             {
398                 sink.link( parseIssueLink( currentIssueId, system ) );
399 
400                 sink.text( currentIssueId );
401 
402                 sink.link_();
403             }
404 
405             if ( iterator.hasNext() )
406             {
407                 sink.text( ", " );
408             }
409         }
410     }
411 
412     /**
413      * Construct a text that references (but does not link to) the issues that were solved by an action.
414      *
415      * @param issue The issue specified by attributes
416      * @param sink The sink
417      * @param fixes The List of issues specified as fixes elements
418      */
419     private void constructIssueText( String issue, Sink sink, List<FixedIssue> fixes )
420     {
421         if ( StringUtils.isNotEmpty( issue ) )
422         {
423             sink.text( issue );
424 
425             if ( !fixes.isEmpty() )
426             {
427                 sink.text( ", " );
428             }
429         }
430 
431         for ( Iterator<FixedIssue> iterator = fixes.iterator(); iterator.hasNext(); )
432         {
433             FixedIssue fixedIssue = iterator.next();
434 
435             String currentIssueId = fixedIssue.getIssue();
436             if ( StringUtils.isNotEmpty( currentIssueId ) )
437             {
438                 sink.text( currentIssueId );
439             }
440 
441             if ( iterator.hasNext() )
442             {
443                 sink.text( ", " );
444             }
445         }
446     }
447 
448     private void constructReleaseHistory( Sink sink, ResourceBundle bundle, List<Release> releaseList )
449     {
450         sink.section2();
451 
452         sink.sectionTitle2();
453         sink.text( bundle.getString( "report.changes.label.releasehistory" ) );
454         sink.sectionTitle2_();
455 
456         sink.table();
457 
458         sink.tableRow();
459 
460         sinkHeader( sink, bundle.getString( "report.issues.label.fixVersion" ) );
461 
462         sinkHeader( sink, bundle.getString( "report.changes.label.releaseDate" ) );
463 
464         sinkHeader( sink, bundle.getString( "report.changes.label.releaseDescription" ) );
465 
466         sink.tableRow_();
467 
468         for ( Release release : releaseList )
469         {
470             sink.tableRow();
471 
472             sinkCellLink( sink, release.getVersion(), "#" + HtmlTools.encodeId( release.getVersion() ) );
473 
474             sinkCell( sink, release.getDateRelease() );
475 
476             sinkCell( sink, release.getDescription() );
477 
478             sink.tableRow_();
479         }
480 
481         sink.table_();
482 
483         // MCHANGES-46
484         if ( linkToFeed )
485         {
486             sink.paragraph();
487             sink.text( bundle.getString( "report.changes.text.rssfeed" ) );
488             sink.nonBreakingSpace();
489             sink.link( "changes.rss" );
490             sinkFigure( sink, "images/rss.png", "rss feed" );
491             sink.link_();
492             sink.paragraph_();
493         }
494 
495         sink.section2_();
496     }
497 
498     /**
499      * Constructs document sections for each of specified releases.
500      *
501      * @param sink Sink
502      * @param bundle Resource bundle
503      * @param releaseList Releases to create content for
504      */
505     private void constructReleases( Sink sink, ResourceBundle bundle, List<Release> releaseList )
506     {
507         for ( Release release : releaseList )
508         {
509             constructRelease( sink, bundle, release );
510         }
511     }
512 
513     /**
514      * Constructs document section for specified release.
515      *
516      * @param sink Sink
517      * @param bundle Resource bundle
518      * @param release Release to create document section for
519      */
520     private void constructRelease( Sink sink, ResourceBundle bundle, Release release )
521     {
522         sink.section2();
523 
524         final String date = ( release.getDateRelease() == null ) ? "" : " \u2013 " + release.getDateRelease();
525 
526         SinkEventAttributes attrs = new SinkEventAttributeSet();
527         attrs.addAttribute( SinkEventAttributes.ID, HtmlTools.encodeId( release.getVersion() ) );
528         sink.sectionTitle( Sink.SECTION_LEVEL_2, attrs );
529         sink.text( bundle.getString( "report.changes.label.release" ) + " " + release.getVersion() + date );
530         sink.sectionTitle_( Sink.SECTION_LEVEL_2 );
531 
532         if ( isReleaseEmpty( release ) )
533         {
534             sink.paragraph();
535             sink.text( bundle.getString( "report.changes.text.no.changes" ) );
536             sink.paragraph_();
537         }
538         else
539         {
540             sink.table();
541 
542             sink.tableRow();
543             sinkHeader( sink, bundle.getString( "report.issues.label.type" ) );
544             sinkHeader( sink, bundle.getString( "report.issues.label.summary" ) );
545             sinkHeader( sink, bundle.getString( "report.issues.label.assignee" ) );
546             if ( this.isAddActionDate() )
547             {
548                 sinkHeader( sink, bundle.getString( "report.issues.label.updated" ) );
549             }
550             sink.tableRow_();
551 
552             for ( Action action : release.getActions() )
553             {
554                 constructAction( sink, bundle, action );
555             }
556 
557             for ( Object o : release.getComponents() )
558             {
559                 Component component = (Component) o;
560                 constructComponent( sink, bundle, component );
561             }
562 
563             sink.table_();
564         }
565 
566         sink.section2_();
567     }
568 
569     /**
570      * Constructs table rows for specified release component. It will create header row for component name and action
571      * rows for all component issues.
572      *
573      * @param sink Sink
574      * @param bundle Resource bundle
575      * @param component Release component to generate content for.
576      */
577     private void constructComponent( Sink sink, ResourceBundle bundle, Component component )
578     {
579         if ( !component.getActions().isEmpty() )
580         {
581             sink.tableRow();
582 
583             sink.tableHeaderCell();
584             sink.tableHeaderCell_();
585 
586             sink.tableHeaderCell();
587             sink.text( component.getName() );
588             sink.tableHeaderCell_();
589 
590             sink.tableHeaderCell();
591             sink.tableHeaderCell_();
592 
593             if ( isAddActionDate() )
594             {
595                 sink.tableHeaderCell();
596                 sink.tableHeaderCell_();
597             }
598 
599             sink.tableRow_();
600 
601             for ( Action action : component.getActions() )
602             {
603                 constructAction( sink, bundle, action );
604             }
605         }
606     }
607 
608     /**
609      * Checks if specified release contains own issues or issues inside the child components.
610      *
611      * @param release Release to check
612      * @return <code>true</code> if release doesn't contain any issues, <code>false</code> otherwise
613      */
614     private boolean isReleaseEmpty( Release release )
615     {
616         if ( !release.getActions().isEmpty() )
617         {
618             return false;
619         }
620 
621         for ( Object o : release.getComponents() )
622         {
623             Component component = (Component) o;
624             if ( !component.getActions().isEmpty() )
625             {
626                 return false;
627             }
628         }
629 
630         return true;
631     }
632 
633     /**
634      * Replace tokens in the issue link template with the real values.
635      *
636      * @param issue The issue identifier
637      * @param system The issue management system
638      * @return An interpolated issue link
639      */
640     private String parseIssueLink( String issue, String system )
641     {
642         String parseLink;
643         String issueLink = (String) this.issueLinksPerSystem.get( system );
644         parseLink = issueLink.replaceFirst( ISSUE_TOKEN, issue );
645         if ( parseLink.contains( URL_TOKEN ) )
646         {
647             String url = this.url.substring( 0, this.url.lastIndexOf( "/" ) );
648             parseLink = parseLink.replaceFirst( URL_TOKEN, url );
649         }
650 
651         return parseLink;
652     }
653 
654 }