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 <project>/<issueManagement> 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 }