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 1763092 2016-10-02 18:42:02Z gboue $
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         String actionDescription = action.getAction();
253 
254         if ( escapeHTML )
255         {
256             sink.text( actionDescription );
257         }
258         else
259         {
260             sink.rawText( actionDescription );
261         }
262 
263         // no null check needed classes from modello return a new ArrayList
264         if ( StringUtils.isNotEmpty( action.getIssue() ) || ( !action.getFixedIssues().isEmpty() ) )
265         {
266             if ( StringUtils.isNotBlank( actionDescription ) && !actionDescription.endsWith( "." ) )
267             {
268                 sink.text( "." );
269             }
270             sink.text( " " + bundle.getString( "report.changes.text.fixes" ) + " " );
271 
272             // Try to get the issue management system specified in the changes.xml file
273             String system = action.getSystem();
274             // Try to get the issue management system configured in the POM
275             if ( StringUtils.isEmpty( system ) )
276             {
277                 system = this.system;
278             }
279             // Use the default issue management system
280             if ( StringUtils.isEmpty( system ) )
281             {
282                 system = DEFAULT_ISSUE_SYSTEM_KEY;
283             }
284             if ( !canGenerateIssueLinks( system ) )
285             {
286                 constructIssueText( action.getIssue(), sink, action.getFixedIssues() );
287             }
288             else
289             {
290                 constructIssueLink( action.getIssue(), system, sink, action.getFixedIssues() );
291             }
292             sink.text( "." );
293         }
294 
295         if ( StringUtils.isNotEmpty( action.getDueTo() ) || ( !action.getDueTos().isEmpty() ) )
296         {
297             constructDueTo( sink, action, bundle, action.getDueTos() );
298         }
299 
300         sink.tableCell_();
301 
302         if ( NO_TEAMLIST.equals( teamlist ) )
303         {
304             sinkCell( sink, action.getDev() );
305         }
306         else
307         {
308             sinkCellLink( sink, action.getDev(), teamlist + "#" + action.getDev() );
309         }
310 
311         if ( this.isAddActionDate() )
312         {
313             sinkCell( sink, action.getDate() );
314         }
315 
316         sink.tableRow_();
317     }
318 
319     /**
320      * Construct a text or link that mention the people that helped with an action.
321      *
322      * @param sink The sink
323      * @param action The action that was done
324      * @param bundle A resource bundle for i18n
325      * @param dueTos Other people that helped with an action
326      */
327     private void constructDueTo( Sink sink, Action action, ResourceBundle bundle, List<DueTo> dueTos )
328     {
329 
330         // Create a Map with key : dueTo name, value : dueTo email
331         Map<String, String> namesEmailMap = new LinkedHashMap<String, String>();
332 
333         // Only add the dueTo specified as attributes, if it has either a dueTo or a dueToEmail
334         if ( StringUtils.isNotEmpty( action.getDueTo() ) || StringUtils.isNotEmpty( action.getDueToEmail() ) )
335         {
336             namesEmailMap.put( action.getDueTo(), action.getDueToEmail() );
337         }
338 
339         for ( DueTo dueTo : dueTos )
340         {
341             namesEmailMap.put( dueTo.getName(), dueTo.getEmail() );
342         }
343 
344         if ( namesEmailMap.isEmpty() )
345         {
346             return;
347         }
348 
349         sink.text( " " + bundle.getString( "report.changes.text.thanx" ) + " " );
350         int i = 0;
351         for ( String currentDueTo : namesEmailMap.keySet() )
352         {
353             String currentDueToEmail = namesEmailMap.get( currentDueTo );
354             i++;
355 
356             if ( StringUtils.isNotEmpty( currentDueToEmail ) )
357             {
358                 sinkLink( sink, currentDueTo, "mailto:" + currentDueToEmail );
359             }
360             else if ( StringUtils.isNotEmpty( currentDueTo ) )
361             {
362                 sink.text( currentDueTo );
363             }
364 
365             if ( i < namesEmailMap.size() )
366             {
367                 sink.text( ", " );
368             }
369         }
370 
371         sink.text( "." );
372     }
373 
374     /**
375      * Construct links to the issues that were solved by an action.
376      *
377      * @param issue The issue specified by attributes
378      * @param system The issue management system
379      * @param sink The sink
380      * @param fixes The List of issues specified as fixes elements
381      */
382     private void constructIssueLink( String issue, String system, Sink sink, List<FixedIssue> fixes )
383     {
384         if ( StringUtils.isNotEmpty( issue ) )
385         {
386             sink.link( parseIssueLink( issue, system ) );
387 
388             sink.text( issue );
389 
390             sink.link_();
391 
392             if ( !fixes.isEmpty() )
393             {
394                 sink.text( ", " );
395             }
396         }
397 
398         for ( Iterator<FixedIssue> iterator = fixes.iterator(); iterator.hasNext(); )
399         {
400             FixedIssue fixedIssue = iterator.next();
401             String currentIssueId = fixedIssue.getIssue();
402             if ( StringUtils.isNotEmpty( currentIssueId ) )
403             {
404                 sink.link( parseIssueLink( currentIssueId, system ) );
405 
406                 sink.text( currentIssueId );
407 
408                 sink.link_();
409             }
410 
411             if ( iterator.hasNext() )
412             {
413                 sink.text( ", " );
414             }
415         }
416     }
417 
418     /**
419      * Construct a text that references (but does not link to) the issues that were solved by an action.
420      *
421      * @param issue The issue specified by attributes
422      * @param sink The sink
423      * @param fixes The List of issues specified as fixes elements
424      */
425     private void constructIssueText( String issue, Sink sink, List<FixedIssue> fixes )
426     {
427         if ( StringUtils.isNotEmpty( issue ) )
428         {
429             sink.text( issue );
430 
431             if ( !fixes.isEmpty() )
432             {
433                 sink.text( ", " );
434             }
435         }
436 
437         for ( Iterator<FixedIssue> iterator = fixes.iterator(); iterator.hasNext(); )
438         {
439             FixedIssue fixedIssue = iterator.next();
440 
441             String currentIssueId = fixedIssue.getIssue();
442             if ( StringUtils.isNotEmpty( currentIssueId ) )
443             {
444                 sink.text( currentIssueId );
445             }
446 
447             if ( iterator.hasNext() )
448             {
449                 sink.text( ", " );
450             }
451         }
452     }
453 
454     private void constructReleaseHistory( Sink sink, ResourceBundle bundle, List<Release> releaseList )
455     {
456         sink.section2();
457 
458         sink.sectionTitle2();
459         sink.text( bundle.getString( "report.changes.label.releasehistory" ) );
460         sink.sectionTitle2_();
461 
462         sink.table();
463 
464         sink.tableRow();
465 
466         sinkHeader( sink, bundle.getString( "report.issues.label.fixVersion" ) );
467 
468         sinkHeader( sink, bundle.getString( "report.changes.label.releaseDate" ) );
469 
470         sinkHeader( sink, bundle.getString( "report.changes.label.releaseDescription" ) );
471 
472         sink.tableRow_();
473 
474         for ( Release release : releaseList )
475         {
476             sink.tableRow();
477 
478             sinkCellLink( sink, release.getVersion(), "#" + HtmlTools.encodeId( release.getVersion() ) );
479 
480             sinkCell( sink, release.getDateRelease() );
481 
482             sinkCell( sink, release.getDescription() );
483 
484             sink.tableRow_();
485         }
486 
487         sink.table_();
488 
489         // MCHANGES-46
490         if ( linkToFeed )
491         {
492             sink.paragraph();
493             sink.text( bundle.getString( "report.changes.text.rssfeed" ) );
494             sink.nonBreakingSpace();
495             sink.link( "changes.rss" );
496             sinkFigure( sink, "images/rss.png", "rss feed" );
497             sink.link_();
498             sink.paragraph_();
499         }
500 
501         sink.section2_();
502     }
503 
504     /**
505      * Constructs document sections for each of specified releases.
506      *
507      * @param sink Sink
508      * @param bundle Resource bundle
509      * @param releaseList Releases to create content for
510      */
511     private void constructReleases( Sink sink, ResourceBundle bundle, List<Release> releaseList )
512     {
513         for ( Release release : releaseList )
514         {
515             constructRelease( sink, bundle, release );
516         }
517     }
518 
519     /**
520      * Constructs document section for specified release.
521      *
522      * @param sink Sink
523      * @param bundle Resource bundle
524      * @param release Release to create document section for
525      */
526     private void constructRelease( Sink sink, ResourceBundle bundle, Release release )
527     {
528         sink.section2();
529 
530         final String date = ( release.getDateRelease() == null ) ? "" : " \u2013 " + release.getDateRelease();
531 
532         SinkEventAttributes attrs = new SinkEventAttributeSet();
533         attrs.addAttribute( SinkEventAttributes.ID, HtmlTools.encodeId( release.getVersion() ) );
534         sink.sectionTitle( Sink.SECTION_LEVEL_2, attrs );
535         sink.text( bundle.getString( "report.changes.label.release" ) + " " + release.getVersion() + date );
536         sink.sectionTitle_( Sink.SECTION_LEVEL_2 );
537 
538         if ( isReleaseEmpty( release ) )
539         {
540             sink.paragraph();
541             sink.text( bundle.getString( "report.changes.text.no.changes" ) );
542             sink.paragraph_();
543         }
544         else
545         {
546             sink.table();
547 
548             sink.tableRow();
549             sinkHeader( sink, bundle.getString( "report.issues.label.type" ) );
550             sinkHeader( sink, bundle.getString( "report.issues.label.summary" ) );
551             sinkHeader( sink, bundle.getString( "report.issues.label.assignee" ) );
552             if ( this.isAddActionDate() )
553             {
554                 sinkHeader( sink, bundle.getString( "report.issues.label.updated" ) );
555             }
556             sink.tableRow_();
557 
558             for ( Action action : release.getActions() )
559             {
560                 constructAction( sink, bundle, action );
561             }
562 
563             for ( Object o : release.getComponents() )
564             {
565                 Component component = (Component) o;
566                 constructComponent( sink, bundle, component );
567             }
568 
569             sink.table_();
570         }
571 
572         sink.section2_();
573     }
574 
575     /**
576      * Constructs table rows for specified release component. It will create header row for component name and action
577      * rows for all component issues.
578      *
579      * @param sink Sink
580      * @param bundle Resource bundle
581      * @param component Release component to generate content for.
582      */
583     private void constructComponent( Sink sink, ResourceBundle bundle, Component component )
584     {
585         if ( !component.getActions().isEmpty() )
586         {
587             sink.tableRow();
588 
589             sink.tableHeaderCell();
590             sink.tableHeaderCell_();
591 
592             sink.tableHeaderCell();
593             sink.text( component.getName() );
594             sink.tableHeaderCell_();
595 
596             sink.tableHeaderCell();
597             sink.tableHeaderCell_();
598 
599             if ( isAddActionDate() )
600             {
601                 sink.tableHeaderCell();
602                 sink.tableHeaderCell_();
603             }
604 
605             sink.tableRow_();
606 
607             for ( Action action : component.getActions() )
608             {
609                 constructAction( sink, bundle, action );
610             }
611         }
612     }
613 
614     /**
615      * Checks if specified release contains own issues or issues inside the child components.
616      *
617      * @param release Release to check
618      * @return <code>true</code> if release doesn't contain any issues, <code>false</code> otherwise
619      */
620     private boolean isReleaseEmpty( Release release )
621     {
622         if ( !release.getActions().isEmpty() )
623         {
624             return false;
625         }
626 
627         for ( Object o : release.getComponents() )
628         {
629             Component component = (Component) o;
630             if ( !component.getActions().isEmpty() )
631             {
632                 return false;
633             }
634         }
635 
636         return true;
637     }
638 
639     /**
640      * Replace tokens in the issue link template with the real values.
641      *
642      * @param issue The issue identifier
643      * @param system The issue management system
644      * @return An interpolated issue link
645      */
646     private String parseIssueLink( String issue, String system )
647     {
648         String parseLink;
649         String issueLink = (String) this.issueLinksPerSystem.get( system );
650         parseLink = issueLink.replaceFirst( ISSUE_TOKEN, issue );
651         if ( parseLink.contains( URL_TOKEN ) )
652         {
653             String url = this.url.substring( 0, this.url.lastIndexOf( "/" ) );
654             parseLink = parseLink.replaceFirst( URL_TOKEN, url );
655         }
656 
657         return parseLink;
658     }
659 
660 }