View Javadoc

1   package org.apache.maven.plugin.announcement;
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.io.File;
23  import java.io.FileOutputStream;
24  import java.io.OutputStreamWriter;
25  import java.io.Writer;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Map;
30  
31  import org.apache.maven.plugin.MojoExecutionException;
32  import org.apache.maven.plugin.changes.ChangesXML;
33  import org.apache.maven.plugin.changes.IssueAdapter;
34  import org.apache.maven.plugin.changes.ProjectUtils;
35  import org.apache.maven.plugin.changes.ReleaseUtils;
36  import org.apache.maven.plugin.issues.Issue;
37  import org.apache.maven.plugin.issues.IssueUtils;
38  import org.apache.maven.plugin.trac.TracDownloader;
39  import org.apache.maven.plugins.changes.model.Release;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.settings.Settings;
42  import org.apache.velocity.Template;
43  import org.apache.velocity.VelocityContext;
44  import org.apache.velocity.app.VelocityEngine;
45  import org.apache.velocity.context.Context;
46  import org.apache.velocity.exception.ResourceNotFoundException;
47  import org.apache.velocity.exception.VelocityException;
48  import org.codehaus.plexus.util.ReaderFactory;
49  import org.codehaus.plexus.util.StringUtils;
50  import org.codehaus.plexus.velocity.VelocityComponent;
51  
52  /**
53   * Goal which generate the template for an announcement.
54   *
55   * @goal announcement-generate
56   * @requiresDependencyResolution test
57   * @author aramirez@exist.com
58   * @version $Id: AnnouncementMojo.html 816601 2012-05-08 12:50:18Z hboutemy $
59   * @since 2.0-beta-2
60   * @threadSafe
61   */
62  public class AnnouncementMojo
63      extends AbstractAnnouncementMojo
64  {
65      private static final String CHANGES_XML = "changes.xml";
66  
67      private static final String JIRA = "JIRA";
68  
69      private static final String TRAC = "Trac";
70  
71      /**
72       * The name of the file which will contain the generated announcement. If
73       * no value is specified the plugin will use the name of the template.
74       *
75       * @parameter expression="${changes.announcementFile}"
76       * @since 2.4
77       */
78      private String announcementFile;
79  
80      /**
81       * Map of custom parameters for the announcement.
82       * This Map will be passed to the template.
83       *
84       * @parameter
85       * @since 2.1
86       */
87      private Map announceParameters;
88  
89      /**
90       * @parameter expression="${project.artifactId}"
91       * @readonly
92       */
93      private String artifactId;
94  
95      /**
96       * Name of the team that develops the artifact.
97       * This parameter will be passed to the template.
98       *
99       * @parameter default-value="${project.name} team" expression="${changes.developmentTeam}"
100      * @required
101      */
102     private String developmentTeam;
103 
104     /**
105      * The name of the artifact to be used in the announcement.
106      *
107      * @parameter expression="${changes.finalName}" default-value="${project.build.finalName}"
108      * @required
109      */
110     private String finalName;
111 
112     /**
113      * @parameter expression="${project.groupId}"
114      * @readonly
115      */
116     private String groupId;
117 
118     /**
119      * Short description or introduction of the released artifact.
120      * This parameter will be passed to the template.
121      *
122      * @parameter default-value="${project.description}"
123      */
124     private String introduction;
125 
126     /**
127      * A list of issue management systems to fetch releases from. This parameter
128      * replaces the parameters <code>generateJiraAnnouncement</code> and
129      * <code>jiraMerge</code>.
130      * <p>
131      * Valid values are: <code>changes.xml</code> and <code>JIRA</code>.
132      * </p>
133      * <strong>Note:</strong> Only one issue management system that is
134      * configured in &lt;project&gt;/&lt;issueManagement&gt; can be used. This
135      * currently means that you can combine a changes.xml file with one other
136      * issue management system.
137      *
138      * @parameter
139      * @since 2.4
140      */
141     private List<String> issueManagementSystems;
142 
143     /**
144      * Directory where the template file will be generated.
145      *
146      * @parameter expression="${project.build.directory}/announcement"
147      * @required
148      */
149     private File outputDirectory;
150 
151     /**
152      * Packaging structure for the artifact.
153      *
154      * @parameter expression="${project.packaging}"
155      * @readonly
156      */
157     private String packaging;
158 
159     /**
160      * The Maven Project.
161      *
162      * @parameter expression="${project}"
163      * @required
164      * @readonly
165      */
166     private MavenProject project;
167 
168     /**
169      * The Velocity template used to format the announcement.
170      *
171      * @parameter default-value="announcement.vm" expression="${changes.template}"
172      * @required
173      */
174     private String template;
175 
176     /**
177      * Directory that contains the template.
178      * <p>
179      * <b>Note:</b> This directory must be a subdirectory of
180      * <code>/src/main/resources/ or current project base directory</code>.
181      * </p>
182      *
183      * @parameter default-value="org/apache/maven/plugin/announcement" expression="${changes.templateDirectory}"
184      * @required
185      */
186     private String templateDirectory;
187 
188     /**
189      * The template encoding.
190      *
191      * @parameter expression="${changes.templateEncoding}" default-value="${project.build.sourceEncoding}"
192      * @since 2.1
193      */
194     private String templateEncoding;
195 
196     /**
197      * Distribution URL of the artifact.
198      * This parameter will be passed to the template.
199      *
200      * @parameter expression="${project.url}"
201      */
202     private String url;
203 
204     /**
205      * URL where the artifact can be downloaded. If not specified,
206      * no URL is used.
207      * This parameter will be passed to the template.
208      *
209      * @parameter
210      */
211     private String urlDownload;
212 
213     /**
214      * Velocity Component.
215      *
216      * @component role="org.codehaus.plexus.velocity.VelocityComponent" roleHint="maven-changes-plugin"
217      * @readonly
218      */
219     private VelocityComponent velocity;
220     /**
221      * Version of the artifact.
222      *
223      * @parameter expression="${changes.version}" default-value="${project.version}"
224      * @required
225      */
226     private String version;
227     /**
228      * The path of the changes.xml file.
229      *
230      * @parameter expression="${basedir}/src/changes/changes.xml"
231      * @required
232      */
233     private File xmlPath;
234 
235     //=======================================//
236     //  JIRA-Announcement Needed Parameters  //
237     //=======================================//
238 
239     /**
240      * Defines the filter parameters to restrict which issues are retrieved
241      * from JIRA. The filter parameter uses the same format of url
242      * parameters that is used in a JIRA search.
243      *
244      * @parameter default-value=""
245      * @since 2.4
246      */
247     private String filter;
248 
249     /**
250      * Flag to determine if the plugin will generate a JIRA announcement.
251      *
252      * @parameter expression="${generateJiraAnnouncement}" default-value="false"
253      * @required
254      * @deprecated Since version 2.4 this parameter has been deprecated. Please use the issueManagementSystems parameter instead.
255      */
256     private boolean generateJiraAnnouncement;
257 
258     /**
259      * If releases from JIRA should be merged with the releases from a
260      * changes.xml file.
261      *
262      * @parameter expression="${changes.jiraMerge}" default-value="false"
263      * @since 2.1
264      * @deprecated Since version 2.4 this parameter has been deprecated. Please use the issueManagementSystems parameter instead.
265      */
266     private boolean jiraMerge;
267 
268     /**
269      * Defines the JIRA password for authentication into a private JIRA installation.
270      *
271      * @parameter default-value="" expression="${changes.jiraPassword}"
272      * @since 2.1
273      */
274     private String jiraPassword;
275 
276     /**
277      * Defines the JIRA username for authentication into a private JIRA installation.
278      *
279      * @parameter default-value="" expression="${changes.jiraUser}"
280      * @since 2.1
281      */
282     private String jiraUser;
283 
284     /**
285      * Path to the JIRA XML file, which will be parsed.
286      *
287      * @parameter expression="${project.build.directory}/jira-announcement.xml"
288      * @required
289      * @readonly
290      */
291     private File jiraXML;
292 
293     /**
294      * The maximum number of issues to fetch from JIRA.
295      * <p>
296      * <b>Note:</b> In versions 2.0-beta-3 and earlier this parameter was
297      * called "nbEntries".
298      * </p>
299      *
300      * @parameter default-value="25"  expression="${changes.maxEntries}"
301      * @required
302      */
303     private int maxEntries;
304 
305     /**
306      * Include issues from JIRA with these resolution ids. Multiple resolution
307      * ids can be specified as a comma separated list of ids.
308      * <p>
309      * <b>Note:</b> In versions 2.0-beta-3 and earlier this parameter was
310      * called "resolutionId".
311      * </p>
312      *
313      * @parameter default-value="Fixed" expression="${changes.resolutionIds}"
314      */
315     private String resolutionIds;
316 
317     /**
318      * Settings XML configuration.
319      *
320      * @parameter expression="${settings}"
321      * @required
322      * @readonly
323      */
324     private Settings settings;
325 
326     /**
327      * Include issues from JIRA with these status ids. Multiple status ids can
328      * be specified as a comma separated list of ids.
329      * <p>
330      * <b>Note:</b> In versions 2.0-beta-3 and earlier this parameter was
331      * called "statusId".
332      * </p>
333      *
334      * @parameter default-value="Closed" expression="${changes.statusIds}"
335      */
336     private String statusIds;
337 
338     /**
339      * Defines the http user for basic authentication into the JIRA webserver.
340      *
341      * @parameter default-value=""
342      * @since 2.4
343      */
344     private String webUser;
345 
346     /**
347      * Defines the http password for basic authentication into the JIRA webserver.
348      *
349      * @parameter default-value=""
350      * @since 2.4
351      */
352     private String webPassword;
353 
354     /**
355      * The prefix used when naming versions in JIRA.
356      * <p>
357      * If you have a project in JIRA with several components that have different
358      * release cycles, it is an often used pattern to prefix the version with
359      * the name of the component, e.g. maven-filtering-1.0 etc. To fetch issues
360      * from JIRA for a release of the "maven-filtering" component you would need
361      * to set this parameter to "maven-filtering-".
362      * </p>
363      *
364      * @parameter default-value=""
365      * @since 2.5
366      */
367     private String versionPrefix;
368 
369     //=======================================//
370     //  Trac Parameters                      //
371     //=======================================//
372 
373     /**
374      * Defines the Trac password for authentication into a private Trac
375      * installation.
376      *
377      * @parameter default-value="" expression="${changes.tracPassword}"
378      * @since 2.4
379      */
380     private String tracPassword;
381 
382     /**
383      * Defines the Trac query for searching for tickets.
384      *
385      * @parameter default-value="order=id"
386      * @since 2.4
387      */
388     private String tracQuery;
389 
390     /**
391      * Defines the Trac username for authentication into a private Trac
392      * installation.
393      *
394      * @parameter default-value="" expression="${changes.tracUser}"
395      * @since 2.4
396      */
397     private String tracUser;
398 
399     private ReleaseUtils releaseUtils = new ReleaseUtils( getLog() );
400 
401     private ChangesXML xml;
402 
403     //=======================================//
404     //    announcement-generate execution    //
405     //=======================================//
406 
407     /**
408      * Generate the template
409      *
410      * @throws MojoExecutionException
411      */
412     public void execute()
413         throws MojoExecutionException
414     {
415         // Run only at the execution root
416         if ( runOnlyAtExecutionRoot && !isThisTheExecutionRoot() )
417         {
418             getLog().info( "Skipping the announcement generation in this project because it's not the Execution Root" );
419         }
420         else
421         {
422             if ( issueManagementSystems == null )
423             {
424                 issueManagementSystems = new ArrayList<String>();
425             }
426 
427             // Handle deprecated parameters, in a backward compatible way
428             if ( issueManagementSystems.isEmpty() )
429             {
430                 if ( this.jiraMerge )
431                 {
432                     issueManagementSystems.add( CHANGES_XML );
433                     issueManagementSystems.add( JIRA );
434                 }
435                 else if ( generateJiraAnnouncement )
436                 {
437                     issueManagementSystems.add( JIRA );
438                 }
439                 else
440                 {
441                     issueManagementSystems.add( CHANGES_XML );
442                 }
443             }
444 
445             // Fetch releases from the configured issue management systems
446             List<Release> releases = null;
447 
448             if ( issueManagementSystems.contains( CHANGES_XML ) )
449             {
450                 if ( getXmlPath().exists() )
451                 {
452                     ChangesXML changesXML = new ChangesXML( getXmlPath(), getLog() );
453                     List<Release> changesReleases = releaseUtils.convertReleaseList( changesXML.getReleaseList() );
454                     releases = releaseUtils.mergeReleases( releases, changesReleases );
455                     getLog().info( "Including issues from file " + getXmlPath() + " in announcement..." );
456                 }
457                 else
458                 {
459                     getLog().warn( "changes.xml file " + getXmlPath().getAbsolutePath() + " does not exist." );
460                 }
461             }
462 
463             if ( issueManagementSystems.contains( JIRA ) )
464             {
465                 if ( ProjectUtils.validateIfIssueManagementComplete( project, JIRA, "JIRA announcement", getLog() ) )
466                 {
467                     List<Release> jiraReleases = getJiraReleases();
468                     releases = releaseUtils.mergeReleases( releases, jiraReleases );
469                     getLog().info( "Including issues from JIRA in announcement..." );
470                 }
471                 else
472                 {
473                     throw new MojoExecutionException(
474                         "Something is wrong with the Issue Management section." + " See previous error messages." );
475                 }
476             }
477 
478             if ( issueManagementSystems.contains( TRAC ) )
479             {
480                 if ( ProjectUtils.validateIfIssueManagementComplete( project, TRAC, "Trac announcement", getLog() ) )
481                 {
482                     List<Release> tracReleases = getTracReleases();
483                     releases = releaseUtils.mergeReleases( releases, tracReleases );
484                     getLog().info( "Including issues from Trac in announcement..." );
485                 }
486                 else
487                 {
488                     throw new MojoExecutionException(
489                         "Something is wrong with the Issue Management section." + " See previous error messages." );
490                 }
491             }
492 
493             // @todo Add more issue management systems here.
494 
495             // Follow these steps:
496             // 1. Add a constant for the name of the issue management system
497             // 2. Add the @parameters needed to configure the issue management system
498             // 3. Add a protected List get<IMSname>Releases() method that retrieves a list of releases
499             // 4. Merge those releases into the "releases" variable
500             // For help with these steps, you can have a look at how this has been done for JIRA or Trac
501 
502             // Generate the report
503             if ( releases == null || releases.isEmpty() )
504             {
505                 throw new MojoExecutionException(
506                     "No releases found in any of the configured issue management systems." );
507             }
508             else
509             {
510                 doGenerate( releases );
511             }
512         }
513     }
514 
515     /**
516      * Add the parameters to velocity context
517      *
518      * @param releases A <code>List</code> of <code>Release</code>s
519      * @throws MojoExecutionException
520      */
521     public void doGenerate( List<Release> releases )
522         throws MojoExecutionException
523     {
524         String version = ( versionPrefix == null ? "" : versionPrefix ) + getVersion();
525 
526         doGenerate( releases, releaseUtils.getLatestRelease( releases, version )  );
527     }
528 
529     protected void doGenerate( List<Release> releases, Release release )
530         throws MojoExecutionException
531     {
532         try
533         {
534             Context context = new VelocityContext();
535 
536             if ( getIntroduction() == null || getIntroduction().equals( "" ) )
537             {
538                 setIntroduction( getUrl() );
539             }
540 
541             context.put( "releases", releases );
542 
543             context.put( "groupId", getGroupId() );
544 
545             context.put( "artifactId", getArtifactId() );
546 
547             context.put( "version", getVersion() );
548 
549             context.put( "packaging", getPackaging() );
550 
551             context.put( "url", getUrl() );
552 
553             context.put( "release", release );
554 
555             context.put( "introduction", getIntroduction() );
556 
557             context.put( "developmentTeam", getDevelopmentTeam() );
558 
559             context.put( "finalName", getFinalName() );
560 
561             context.put( "urlDownload", getUrlDownload() );
562 
563             context.put( "project", project );
564 
565             if ( announceParameters == null )
566             {
567                 // empty Map to prevent NPE in velocity execution
568                 context.put( "announceParameters", Collections.EMPTY_MAP );
569             }
570             else
571             {
572                 context.put( "announceParameters", announceParameters );
573             }
574 
575 
576             processTemplate( context, getOutputDirectory(), template, announcementFile );
577         }
578         catch ( ResourceNotFoundException rnfe )
579         {
580             throw new MojoExecutionException( "Resource not found.", rnfe );
581         }
582         catch ( VelocityException ve )
583         {
584             throw new MojoExecutionException( ve.toString(), ve );
585         }
586     }
587 
588     /**
589      * Create the velocity template
590      *
591      * @param context velocity context that has the parameter values
592      * @param outputDirectory directory where the file will be generated
593      * @param template velocity template which will the context be merged
594      * @param announcementFile The file name of the generated announcement
595      * @throws ResourceNotFoundException, VelocityException, IOException
596      */
597     public void processTemplate( Context context, File outputDirectory, String template, String announcementFile )
598         throws ResourceNotFoundException, VelocityException, MojoExecutionException
599     {
600         File f;
601 
602         // Use the name of the template as a default value
603         if ( StringUtils.isEmpty( announcementFile ) )
604         {
605             announcementFile = template;
606         }
607 
608         try
609         {
610             f = new File( outputDirectory, announcementFile );
611 
612             if ( !f.getParentFile().exists() )
613             {
614                 f.getParentFile().mkdirs();
615             }
616 
617             VelocityEngine engine = velocity.getEngine();
618 
619             engine.setApplicationAttribute( "baseDirectory", basedir );
620 
621             if ( StringUtils.isEmpty( templateEncoding ) )
622             {
623                 templateEncoding =  ReaderFactory.FILE_ENCODING;
624                 getLog().warn(
625                                "File encoding has not been set, using platform encoding " + templateEncoding
626                                    + ", i.e. build is platform dependent!" );
627             }
628 
629             Writer writer = new OutputStreamWriter( new FileOutputStream( f ), templateEncoding );
630 
631             Template velocityTemplate = engine.getTemplate( templateDirectory + "/" + template, templateEncoding );
632 
633             velocityTemplate.merge( context, writer );
634 
635             writer.flush();
636 
637             writer.close();
638 
639             getLog().info( "Created template " + f );
640         }
641 
642         catch ( ResourceNotFoundException rnfe )
643         {
644             throw new ResourceNotFoundException( "Template not found. ( " + templateDirectory + "/" + template + " )" );
645         }
646         catch ( VelocityException ve )
647         {
648             throw new VelocityException( ve.toString() );
649         }
650 
651         catch ( Exception e )
652         {
653             if ( e.getCause() != null )
654             {
655                 getLog().warn( e.getCause() );
656             }
657             throw new MojoExecutionException( e.toString(), e.getCause() );
658         }
659     }
660 
661     protected List<Release> getJiraReleases()
662         throws MojoExecutionException
663     {
664         JiraDownloader jiraDownloader = new JiraDownloader();
665 
666         File jiraXMLFile = jiraXML;
667 
668         jiraDownloader.setLog( getLog() );
669 
670         jiraDownloader.setOutput( jiraXMLFile );
671 
672         jiraDownloader.setStatusIds( statusIds );
673 
674         jiraDownloader.setResolutionIds( resolutionIds );
675 
676         jiraDownloader.setMavenProject( project );
677 
678         jiraDownloader.setSettings( settings );
679 
680         jiraDownloader.setNbEntries( maxEntries );
681 
682         jiraDownloader.setFilter( filter );
683 
684         jiraDownloader.setJiraUser( jiraUser );
685 
686         jiraDownloader.setJiraPassword( jiraPassword );
687 
688         jiraDownloader.setWebUser( webUser );
689 
690         jiraDownloader.setWebPassword( webPassword );
691 
692         try
693         {
694             jiraDownloader.doExecute();
695 
696             List<Issue> issueList = jiraDownloader.getIssueList();
697 
698             if ( StringUtils.isNotEmpty( versionPrefix ) )
699             {
700                 int originalNumberOfIssues = issueList.size();
701                 issueList = IssueUtils.filterIssuesWithVersionPrefix( issueList, versionPrefix );
702                 getLog().debug( "Filtered out " + issueList.size() + " issues of " + originalNumberOfIssues
703                     + " that matched the versionPrefix '" + versionPrefix + "'." );
704             }
705 
706             return getReleases( issueList );
707         }
708         catch ( Exception e )
709         {
710             throw new MojoExecutionException( "Failed to extract issues from JIRA.", e );
711         }
712     }
713 
714     private List<Release> getReleases( List<Issue> issues )
715     {
716         if ( issues.isEmpty() )
717         {
718             return Collections.emptyList();
719         }
720         else
721         {
722             return IssueAdapter.getReleases( issues );
723         }
724     }
725 
726     protected List<Release> getTracReleases()
727         throws MojoExecutionException
728     {
729         TracDownloader issueDownloader = new TracDownloader();
730 
731         issueDownloader.setProject( project );
732 
733         issueDownloader.setQuery( tracQuery );
734 
735         issueDownloader.setTracPassword( tracPassword );
736 
737         issueDownloader.setTracUser( tracUser );
738 
739         try
740         {
741             return getReleases( issueDownloader.getIssueList() );
742         }
743         catch ( Exception e )
744         {
745             throw new MojoExecutionException( "Failed to extract issues from Trac.", e );
746         }
747     }
748 
749     /*
750      * accessors
751      */
752 
753     public String getArtifactId()
754     {
755         return artifactId;
756     }
757 
758     public void setArtifactId( String artifactId )
759     {
760         this.artifactId = artifactId;
761     }
762 
763     public String getDevelopmentTeam()
764     {
765         return developmentTeam;
766     }
767 
768     public void setDevelopmentTeam( String developmentTeam )
769     {
770         this.developmentTeam = developmentTeam;
771     }
772 
773     public String getFinalName()
774     {
775         return finalName;
776     }
777 
778     public void setFinalName( String finalName )
779     {
780         this.finalName = finalName;
781     }
782 
783     public String getGroupId()
784     {
785         return groupId;
786     }
787 
788     public void setGroupId( String groupId )
789     {
790         this.groupId = groupId;
791     }
792 
793     public String getIntroduction()
794     {
795         return introduction;
796     }
797 
798     public void setIntroduction( String introduction )
799     {
800         this.introduction = introduction;
801     }
802 
803     public File getOutputDirectory()
804     {
805         return outputDirectory;
806     }
807 
808     public void setOutputDirectory( File outputDirectory )
809     {
810         this.outputDirectory = outputDirectory;
811     }
812 
813     public String getPackaging()
814     {
815         return packaging;
816     }
817 
818     public void setPackaging( String packaging )
819     {
820         this.packaging = packaging;
821     }
822 
823     public String getUrl()
824     {
825         return url;
826     }
827 
828     public void setUrl( String url )
829     {
830         this.url = url;
831     }
832 
833     public String getUrlDownload()
834     {
835         return urlDownload;
836     }
837 
838     public void setUrlDownload( String urlDownload )
839     {
840         this.urlDownload = urlDownload;
841     }
842 
843     public VelocityComponent getVelocity()
844     {
845         return velocity;
846     }
847 
848     public void setVelocity( VelocityComponent velocity )
849     {
850         this.velocity = velocity;
851     }
852 
853     public String getVersion()
854     {
855         return version;
856     }
857 
858     public void setVersion( String version )
859     {
860         this.version = version;
861     }
862 
863     public ChangesXML getXml()
864     {
865         return xml;
866     }
867 
868     public void setXml( ChangesXML xml )
869     {
870         this.xml = xml;
871     }
872 
873     public File getXmlPath()
874     {
875         return xmlPath;
876     }
877 
878     public void setXmlPath( File xmlPath )
879     {
880         this.xmlPath = xmlPath;
881     }
882 }