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