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 1591792 2014-05-01 22:08:00Z michaelo $
46   */
47  public class ChangesReportGenerator extends AbstractIssuesReportGenerator
48  {
49  
50      /**
51       * The token in {@link #issueLinksPerSystem} denoting the base URL for the issue management.
52       */
53      private static final String URL_TOKEN = "%URL%";
54  
55      /**
56       * The token in {@link #issueLinksPerSystem} denoting the issue ID.
57       */
58      private static final String ISSUE_TOKEN = "%ISSUE%";
59  
60      static final String DEFAULT_ISSUE_SYSTEM_KEY = "default";
61  
62      private static final String NO_TEAMLIST = "none";
63  
64      /**
65       * The issue management system to use, for actions that do not specify a
66       * 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 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 releaseList;
91  
92      public ChangesReportGenerator()
93      {
94          issueLinksPerSystem = new HashMap();
95      }
96  
97      public ChangesReportGenerator( List 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 getIssueLinksPerSystem()
156     {
157         return issueLinksPerSystem;
158     }
159 
160     public void setIssueLinksPerSystem( Map 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 = (String) 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 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 ( Object dueTo1 : dueTos )
334         {
335             DueTo dueTo = (DueTo) dueTo1;
336             namesEmailMap.put( dueTo.getName(), dueTo.getEmail() );
337         }
338 
339         if ( namesEmailMap.isEmpty() )
340         {
341             return;
342         }
343 
344         sink.text( " " + bundle.getString( "report.changes.text.thanx" ) + " " );
345         int i = 0;
346         for ( String currentDueTo : namesEmailMap.keySet() )
347         {
348             String currentDueToEmail = namesEmailMap.get( currentDueTo );
349             i++;
350 
351             if ( StringUtils.isNotEmpty( currentDueToEmail ) )
352             {
353                 sinkLink( sink, currentDueTo, "mailto:" + currentDueToEmail );
354             }
355             else if ( StringUtils.isNotEmpty( currentDueTo ) )
356             {
357                 sink.text( currentDueTo );
358             }
359 
360             if ( i < namesEmailMap.size() )
361             {
362                 sink.text( ", " );
363             }
364         }
365 
366         sink.text( "." );
367     }
368 
369     /**
370      * Construct links to the issues that were solved by an action.
371      *
372      * @param issue The issue specified by attributes
373      * @param system The issue management system
374      * @param sink The sink
375      * @param fixes The List of issues specified as fixes elements
376      */
377     private void constructIssueLink( String issue, String system, Sink sink, List fixes )
378     {
379         if ( StringUtils.isNotEmpty( issue ) )
380         {
381             sink.link( parseIssueLink( issue, system ) );
382 
383             sink.text( issue );
384 
385             sink.link_();
386 
387             if ( !fixes.isEmpty() )
388             {
389                 sink.text( ", " );
390             }
391         }
392 
393         for ( Iterator iterator = fixes.iterator(); iterator.hasNext(); )
394         {
395             FixedIssue fixedIssue = (FixedIssue) iterator.next();
396             String currentIssueId = fixedIssue.getIssue();
397             if ( StringUtils.isNotEmpty( currentIssueId ) )
398             {
399                 sink.link( parseIssueLink( currentIssueId, system ) );
400 
401                 sink.text( currentIssueId );
402 
403                 sink.link_();
404             }
405 
406             if ( iterator.hasNext() )
407             {
408                 sink.text( ", " );
409             }
410         }
411     }
412 
413     /**
414      * Construct a text that references (but does not link to) the issues that
415      * were solved by an action.
416      *
417      * @param issue The issue specified by attributes
418      * @param sink The sink
419      * @param fixes The List of issues specified as fixes elements
420      */
421     private void constructIssueText( String issue, Sink sink, List fixes )
422     {
423         if ( StringUtils.isNotEmpty( issue ) )
424         {
425             sink.text( issue );
426 
427             if ( !fixes.isEmpty() )
428             {
429                 sink.text( ", " );
430             }
431         }
432 
433         for ( Iterator iterator = fixes.iterator(); iterator.hasNext(); )
434         {
435             FixedIssue fixedIssue = (FixedIssue) iterator.next();
436 
437             String currentIssueId = fixedIssue.getIssue();
438             if ( StringUtils.isNotEmpty( currentIssueId ) )
439             {
440                 sink.text( currentIssueId );
441             }
442 
443             if ( iterator.hasNext() )
444             {
445                 sink.text( ", " );
446             }
447         }
448     }
449 
450     private void constructReleaseHistory( Sink sink, ResourceBundle bundle, List releaseList )
451     {
452         sink.section2();
453 
454         sink.sectionTitle2();
455         sink.text( bundle.getString( "report.changes.label.releasehistory" ) );
456         sink.sectionTitle2_();
457 
458         sink.table();
459 
460         sink.tableRow();
461 
462         sinkHeader( sink, bundle.getString( "report.issues.label.fixVersion" ) );
463 
464         sinkHeader( sink, bundle.getString( "report.changes.label.releaseDate" ) );
465 
466         sinkHeader( sink, bundle.getString( "report.changes.label.releaseDescription" ) );
467 
468         sink.tableRow_();
469 
470         for ( Object aReleaseList : releaseList )
471         {
472             Release release = (Release) aReleaseList;
473 
474             sink.tableRow();
475 
476             sinkCellLink( sink, release.getVersion(), "#" + HtmlTools.encodeId( release.getVersion() ) );
477 
478             sinkCell( sink, release.getDateRelease() );
479 
480             sinkCell( sink, release.getDescription() );
481 
482             sink.tableRow_();
483         }
484 
485         sink.table_();
486 
487         // MCHANGES-46
488         if ( linkToFeed )
489         {
490             sink.paragraph();
491             sink.text( bundle.getString( "report.changes.text.rssfeed" ) );
492             sink.nonBreakingSpace();
493             sink.link( "changes.rss" );
494             sinkFigure( sink, "images/rss.png", "rss feed" );
495             sink.link_();
496             sink.paragraph_();
497         }
498 
499         sink.section2_();
500     }
501 
502     /**
503      * Constructs document sections for each of specified releases.
504      *
505      * @param sink Sink
506      * @param bundle Resource bundle
507      * @param releaseList Releases to create content for
508      */
509     private void constructReleases( Sink sink, ResourceBundle bundle, List releaseList )
510     {
511         for ( Object aReleaseList : releaseList )
512         {
513             Release release = (Release) aReleaseList;
514             constructRelease( sink, bundle, release );
515         }
516     }
517 
518     /**
519      * Constructs document section for specified release.
520      *
521      * @param sink Sink
522      * @param bundle Resource bundle
523      * @param release Release to create document section for
524      */
525     private void constructRelease( Sink sink, ResourceBundle bundle, Release release )
526     {
527         sink.section2();
528 
529         final String date = ( release.getDateRelease() == null ) ? "" : " \u2013 " + release.getDateRelease();
530 
531         SinkEventAttributes attrs = new SinkEventAttributeSet();
532         attrs.addAttribute( SinkEventAttributes.ID, HtmlTools.encodeId( release.getVersion() ) );
533         sink.sectionTitle( Sink.SECTION_LEVEL_2, attrs );
534         sink.text( bundle.getString( "report.changes.label.release" ) + " "
535             + 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             sink.section2_();
572         }
573     }
574 
575     /**
576      * Constructs table rows for specified release component. It will create header row for
577      * component name and action 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 }