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