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