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