package org.apache.maven.plugins.announcement; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.changes.ChangesXML; import org.apache.maven.plugins.changes.IssueAdapter; import org.apache.maven.plugins.changes.ProjectUtils; import org.apache.maven.plugins.changes.ReleaseUtils; import org.apache.maven.plugins.changes.model.Release; import org.apache.maven.plugins.github.GitHubDownloader; import org.apache.maven.plugins.github.GitHubIssueManagementSystem; import org.apache.maven.plugins.issues.Issue; import org.apache.maven.plugins.issues.IssueManagementSystem; import org.apache.maven.plugins.issues.IssueUtils; import org.apache.maven.plugins.jira.AbstractJiraDownloader; import org.apache.maven.plugins.jira.AdaptiveJiraDownloader; import org.apache.maven.plugins.jira.JIRAIssueManagmentSystem; import org.apache.maven.plugins.trac.TracDownloader; import org.apache.maven.plugins.trac.TracIssueManagmentSystem; import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Settings; import org.apache.maven.settings.crypto.SettingsDecrypter; import org.apache.velocity.Template; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.context.Context; import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.exception.VelocityException; import org.apache.velocity.tools.ToolManager; import org.codehaus.plexus.util.ReaderFactory; import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.velocity.VelocityComponent; /** * Goal which generate an announcement from the announcement template. * * @author aramirez@exist.com * @version $Id$ * @since 2.0-beta-2 */ @Mojo( name = "announcement-generate", threadSafe = true ) public class AnnouncementMojo extends AbstractAnnouncementMojo { private static final String CHANGES_XML = "changes.xml"; private static final String JIRA = "JIRA"; private static final String TRAC = "Trac"; private static final String GIT_HUB = "GitHub"; /** * The name of the file which will contain the generated announcement. If no value is specified the plugin will use * the name of the template. * * @since 2.4 */ @Parameter( property = "changes.announcementFile" ) private String announcementFile; /** * Map of custom parameters for the announcement. This Map will be passed to the template. * * @since 2.1 */ @Parameter private Map<Object, Object> announceParameters; /** */ @Parameter( property = "project.artifactId", readonly = true ) private String artifactId; /** * Name of the team that develops the artifact. This parameter will be passed to the template. */ @Parameter( property = "changes.developmentTeam", defaultValue = "${project.name} team", required = true ) private String developmentTeam; /** * The name of the artifact to be used in the announcement. */ @Parameter( property = "changes.finalName", defaultValue = "${project.build.finalName}", required = true ) private String finalName; /** */ @Parameter( property = "project.groupId", readonly = true ) private String groupId; /** * Short description or introduction of the released artifact. This parameter will be passed to the template. */ @Parameter( defaultValue = "${project.description}" ) private String introduction; /** * A list of issue management systems to fetch releases from. This parameter replaces the parameters * <code>generateJiraAnnouncement</code> and <code>jiraMerge</code>. * <p> * Valid values are: <code>changes.xml</code> and <code>JIRA</code>. * </p> * <strong>Note:</strong> Only one issue management system that is configured in * <project>/<issueManagement> can be used. This currently means that you can combine a changes.xml file * with one other issue management system. * * @since 2.4 */ @Parameter private List<String> issueManagementSystems; /** * Maps issues types to action types for grouping issues in announcements. If issue types are not defined for a * action type then the default issue type will be applied. * <p> * Valid action types: <code>add</code>, <code>fix</code> and <code>update</code>. * </p> * * @since 2.6 */ @Parameter private Map<String, String> issueTypes; /** * Directory where the announcement file will be generated. * * @since 2.10 */ @Parameter( defaultValue = "${project.build.directory}/announcement", required = true ) private File announcementDirectory; /** * Directory where the announcement file will be generated. * * @deprecated Starting with version 2.10 this parameter is no longer used. You must use * {@link #announcementDirectory} instead. */ @Parameter private File outputDirectory; /** * Packaging structure for the artifact. */ @Parameter( property = "project.packaging", readonly = true ) private String packaging; /** * The Maven Project. */ @Parameter( defaultValue = "${project}", readonly = true, required = true ) private MavenProject project; /** * The Velocity template used to format the announcement. */ @Parameter( property = "changes.template", defaultValue = "announcement.vm", required = true ) private String template; /** * Directory that contains the template. * <p> * <b>Note:</b> This directory must be a subdirectory of * <code>/src/main/resources/ or current project base directory</code>. * </p> */ // CHECKSTYLE_OFF: LineLength @Parameter( property = "changes.templateDirectory", defaultValue = "org/apache/maven/plugins/announcement", required = true ) private String templateDirectory; // CHECKSTYLE_ON: LineLength /** * The template encoding. * * @since 2.1 */ @Parameter( property = "changes.templateEncoding", defaultValue = "${project.build.sourceEncoding}" ) private String templateEncoding; /** * Use the JIRA query language instead of the JIRA query based on HTTP parameters. From JIRA 5.1 and up only JQL is * supported. JIRA 4.4 supports both JQL and URL parameter based queries. From 5.1.1 this is obsolete, since REST * queries only use JQL. * * @since 2.10 */ @Parameter( property = "changes.useJql", defaultValue = "false" ) private boolean useJql; /** * Distribution URL of the artifact. This parameter will be passed to the template. */ @Parameter( property = "project.url" ) private String url; /** * URL where the artifact can be downloaded. If not specified, no URL is used. This parameter will be passed to the * template. */ @Parameter private String urlDownload; /** * Velocity Component. */ @Component( role = VelocityComponent.class, hint = "maven-changes-plugin" ) private VelocityComponent velocity; /** * Component used to decrypt server information. */ @Component private SettingsDecrypter settingsDecrypter; /** * Version of the artifact. */ @Parameter( property = "changes.version", defaultValue = "${project.version}", required = true ) private String version; /** * The path of the changes.xml file. * * @parameter expression="${basedir}/src/changes/changes.xml" * @required */ @Parameter( defaultValue = "${basedir}/src/changes/changes.xml" ) private File xmlPath; // =======================================// // JIRA-Announcement Needed Parameters // // =======================================// /** * Defines the filter parameters to restrict which issues are retrieved from JIRA. The filter parameter uses the * same format of url parameters that is used in a JIRA search. * * @since 2.4 */ @Parameter( defaultValue = "" ) private String filter; /** * Flag to determine if the plugin will generate a JIRA announcement. * * @deprecated Since version 2.4 this parameter has been deprecated. Please use the issueManagementSystems parameter * instead. */ @Parameter( property = "generateJiraAnnouncement" ) private Boolean generateJiraAnnouncement; /** * If releases from JIRA should be merged with the releases from a changes.xml file. * * @since 2.1 * @deprecated Since version 2.4 this parameter has been deprecated. Please use the issueManagementSystems parameter * instead. */ @Parameter( property = "changes.jiraMerge" ) private Boolean jiraMerge; /** * Defines the JIRA password for authentication into a private JIRA installation. * * @since 2.1 */ @Parameter( property = "changes.jiraPassword", defaultValue = "" ) private String jiraPassword; /** * Defines the JIRA username for authentication into a private JIRA installation. * * @since 2.1 */ @Parameter( property = "changes.jiraUser", defaultValue = "" ) private String jiraUser; /** * Path to the JIRA XML file, which will be parsed. */ @Parameter( defaultValue = "${project.build.directory}/jira-announcement.xml", required = true, readonly = true ) private File jiraXML; /** * The maximum number of issues to fetch from JIRA. * <p> * <b>Note:</b> In versions 2.0-beta-3 and earlier this parameter was called "nbEntries". * </p> */ @Parameter( property = "changes.maxEntries", defaultValue = "25", required = true ) private int maxEntries; /** * Include issues from JIRA with these resolution ids. Multiple resolution ids can be specified as a comma separated * list of ids. * <p> * <b>Note:</b> In versions 2.0-beta-3 and earlier this parameter was called "resolutionId". * </p> */ @Parameter( property = "changes.resolutionIds", defaultValue = "Fixed" ) private String resolutionIds; /** * Settings XML configuration. */ @Parameter( defaultValue = "${settings}", readonly = true, required = true ) private Settings settings; /** * Include issues from JIRA with these status ids. Multiple status ids can be specified as a comma separated list of * ids. * <p> * <b>Note:</b> In versions 2.0-beta-3 and earlier this parameter was called "statusId". * </p> */ @Parameter( property = "changes.statusIds", defaultValue = "Closed" ) private String statusIds; /** * Defines the http user for basic authentication into the JIRA webserver. * * @since 2.4 */ @Parameter( property = "changes.webUser", defaultValue = "" ) private String webUser; /** * Defines the http password for basic authentication into the JIRA webserver. * * @since 2.4 */ @Parameter( property = "changes.webPassword", defaultValue = "" ) private String webPassword; /** * The prefix used when naming versions in JIRA. * <p> * If you have a project in JIRA with several components that have different release cycles, it is an often used * pattern to prefix the version with the name of the component, e.g. maven-filtering-1.0 etc. To fetch issues from * JIRA for a release of the "maven-filtering" component you would need to set this parameter to "maven-filtering-". * </p> * * @since 2.5 */ @Parameter( property = "changes.versionPrefix", defaultValue = "" ) private String versionPrefix; /** * Defines the connection timeout in milliseconds when accessing JIRA's REST-API. * <p> * Might help when you have a lot of different resolutions in your JIRA instance. * </p> * * @since 2.11 */ @Parameter( property = "changes.jiraConnectionTimeout", defaultValue = "36000" ) private int jiraConnectionTimeout; /** * Defines the receive timeout in milliseconds when accessing JIRA's REST-API. * <p> * Might help when you have a lot of different resolutions in your JIRA instance. * </p> * * @since 2.11 */ @Parameter( property = "changes.jiraReceiveTimout", defaultValue = "32000" ) private int jiraReceiveTimout; // =======================================// // Trac Parameters // // =======================================// /** * Defines the Trac password for authentication into a private Trac installation. * * @since 2.4 */ @Parameter( property = "changes.tracPassword", defaultValue = "" ) private String tracPassword; /** * Defines the Trac query for searching for tickets. * * @since 2.4 */ @Parameter( defaultValue = "order=id" ) private String tracQuery; /** * Defines the Trac username for authentication into a private Trac installation. * * @since 2.4 */ @Parameter( property = "changes.tracUser", defaultValue = "" ) private String tracUser; // =======================================// // Github Parameters // // =======================================// /** * The scheme of your github api domain. Only use if using github enterprise. * * @since 2.9 */ @Parameter( defaultValue = "http", property = "changes.githubAPIScheme" ) private String githubAPIScheme; /** * The port of your github api domain. Only use if using github enterprise. * * @since 2.9 */ @Parameter( defaultValue = "80", property = "changes.githubAPIPort" ) private int githubAPIPort; /** * The settings.xml server id to be used to authenticate into github api domain. Only use if using github * enterprise. * * @since 2.12 */ @Parameter( defaultValue = "github" ) private String githubAPIServerId; /** * Boolean which says if we should include open github issues in the announcement. */ @Parameter( defaultValue = "false" ) private boolean includeOpenIssues; private ReleaseUtils releaseUtils = new ReleaseUtils( getLog() ); private ChangesXML xml; // =======================================// // announcement-generate execution // // =======================================// /** * Generate the template * * @throws MojoExecutionException in case of errors */ public void execute() throws MojoExecutionException { // Fail build fast if it is using deprecated parameters failIfUsingDeprecatedParameter( outputDirectory, "outputDirectory", "announcementDirectory" ); failIfUsingDeprecatedParameter( generateJiraAnnouncement, "generateJiraAnnouncement", "issueManagementSystems " ); failIfUsingDeprecatedParameter( jiraMerge, "jiraMerge", "issueManagementSystems " ); // Run only at the execution root if ( runOnlyAtExecutionRoot && !isThisTheExecutionRoot() ) { getLog().info( "Skipping the announcement generation in this project because it's not the Execution Root" ); } else { if ( issueManagementSystems == null ) { issueManagementSystems = new ArrayList<String>(); } if ( issueManagementSystems.isEmpty() ) { issueManagementSystems.add( CHANGES_XML ); } // Fetch releases from the configured issue management systems List<Release> releases = null; if ( issueManagementSystems.contains( CHANGES_XML ) ) { if ( getXmlPath().exists() ) { ChangesXML changesXML = new ChangesXML( getXmlPath(), getLog() ); List<Release> changesReleases = changesXML.getReleaseList(); releases = releaseUtils.mergeReleases( null, changesReleases ); getLog().info( "Including issues from file " + getXmlPath() + " in announcement..." ); } else { getLog().warn( "changes.xml file " + getXmlPath().getAbsolutePath() + " does not exist." ); } } if ( issueManagementSystems.contains( JIRA ) ) { String message = ProjectUtils.validateIssueManagement( project, JIRA, "JIRA announcement" ); if ( message == null ) { List<Release> jiraReleases = getJiraReleases(); releases = releaseUtils.mergeReleases( releases, jiraReleases ); getLog().info( "Including issues from JIRA in announcement..." ); } else { throw new MojoExecutionException( "Something is wrong with the Issue Management section. " + message ); } } if ( issueManagementSystems.contains( TRAC ) ) { String message = ProjectUtils.validateIssueManagement( project, TRAC, "Trac announcement" ); if ( message == null ) { List<Release> tracReleases = getTracReleases(); releases = releaseUtils.mergeReleases( releases, tracReleases ); getLog().info( "Including issues from Trac in announcement..." ); } else { throw new MojoExecutionException( "Something is wrong with the Issue Management section. " + message ); } } if ( issueManagementSystems.contains( GIT_HUB ) ) { String message = ProjectUtils.validateIssueManagement( project, GIT_HUB, "GitHub announcement" ); if ( message == null ) { List<Release> gitHubReleases = getGitHubReleases(); releases = releaseUtils.mergeReleases( releases, gitHubReleases ); getLog().info( "Including issues from GitHub in announcement..." ); } else { throw new MojoExecutionException( "Something is wrong with the Issue Management section. " + message ); } } // @todo Add more issue management systems here. // Follow these steps: // 1. Add a constant for the name of the issue management system // 2. Add the @parameters needed to configure the issue management system // 3. Add a protected List get<IMSname>Releases() method that retrieves a list of releases // 4. Merge those releases into the "releases" variable // For help with these steps, you can have a look at how this has been done for JIRA or Trac // Generate the report if ( releases == null || releases.isEmpty() ) { throw new MojoExecutionException( "No releases found in any of the " + "configured issue management systems." ); } else { doGenerate( releases ); } } } private void failIfUsingDeprecatedParameter( Object value, String name, String replacement ) throws MojoExecutionException { if ( value != null ) { throw new MojoExecutionException( "You are using the old parameter '" + name + "'. " + "You must use '" + replacement + "' instead." ); } } /** * Add the parameters to velocity context * * @param releases A <code>List</code> of <code>Release</code>s * @throws MojoExecutionException in case of errors */ public void doGenerate( List<Release> releases ) throws MojoExecutionException { String version = ( versionPrefix == null ? "" : versionPrefix ) + getVersion(); getLog().debug( "Generating announcement for version [" + version + "]. Found these releases: " + ReleaseUtils.toString( releases ) ); doGenerate( releases, releaseUtils.getLatestRelease( releases, version ) ); } protected void doGenerate( List<Release> releases, Release release ) throws MojoExecutionException { try { ToolManager toolManager = new ToolManager( true ); Context context = toolManager.createContext(); if ( getIntroduction() == null || getIntroduction().equals( "" ) ) { setIntroduction( getUrl() ); } context.put( "releases", releases ); context.put( "groupId", getGroupId() ); context.put( "artifactId", getArtifactId() ); context.put( "version", getVersion() ); context.put( "packaging", getPackaging() ); context.put( "url", getUrl() ); context.put( "release", release ); context.put( "introduction", getIntroduction() ); context.put( "developmentTeam", getDevelopmentTeam() ); context.put( "finalName", getFinalName() ); context.put( "urlDownload", getUrlDownload() ); context.put( "project", project ); if ( announceParameters == null ) { // empty Map to prevent NPE in velocity execution context.put( "announceParameters", Collections.emptyMap() ); } else { context.put( "announceParameters", announceParameters ); } processTemplate( context, announcementDirectory, template, announcementFile ); } catch ( ResourceNotFoundException rnfe ) { throw new MojoExecutionException( "Resource not found.", rnfe ); } catch ( VelocityException ve ) { throw new MojoExecutionException( ve.toString(), ve ); } } /** * Create the velocity template * * @param context velocity context that has the parameter values * @param outputDirectory directory where the file will be generated * @param template velocity template which will the context be merged * @param announcementFile The file name of the generated announcement * @throws VelocityException in case of errors. * @throws MojoExecutionException in case of errors. */ public void processTemplate( Context context, File outputDirectory, String template, String announcementFile ) throws VelocityException, MojoExecutionException { File f; // Use the name of the template as a default value if ( StringUtils.isEmpty( announcementFile ) ) { announcementFile = template; } try { f = new File( outputDirectory, announcementFile ); if ( !f.getParentFile().exists() ) { f.getParentFile().mkdirs(); } VelocityEngine engine = velocity.getEngine(); engine.setApplicationAttribute( "baseDirectory", basedir ); if ( StringUtils.isEmpty( templateEncoding ) ) { templateEncoding = ReaderFactory.FILE_ENCODING; getLog().warn( "File encoding has not been set, using platform encoding " + templateEncoding + ", i.e. build is platform dependent!" ); } Writer writer = new OutputStreamWriter( new FileOutputStream( f ), templateEncoding ); Template velocityTemplate = engine.getTemplate( templateDirectory + "/" + template, templateEncoding ); velocityTemplate.merge( context, writer ); writer.flush(); writer.close(); getLog().info( "Created template " + f ); } catch ( ResourceNotFoundException rnfe ) { throw new ResourceNotFoundException( "Template not found. ( " + templateDirectory + "/" + template + " )" ); } catch ( VelocityException ve ) { throw new VelocityException( ve.toString() ); } catch ( Exception e ) { if ( e.getCause() != null ) { getLog().warn( e.getCause() ); } throw new MojoExecutionException( e.toString(), e.getCause() ); } } protected List<Release> getJiraReleases() throws MojoExecutionException { AbstractJiraDownloader jiraDownloader = new AdaptiveJiraDownloader(); File jiraXMLFile = jiraXML; jiraDownloader.setLog( getLog() ); jiraDownloader.setOutput( jiraXMLFile ); jiraDownloader.setStatusIds( statusIds ); jiraDownloader.setResolutionIds( resolutionIds ); jiraDownloader.setMavenProject( project ); jiraDownloader.setSettings( settings ); jiraDownloader.setNbEntries( maxEntries ); jiraDownloader.setFilter( filter ); jiraDownloader.setJiraUser( jiraUser ); jiraDownloader.setJiraPassword( jiraPassword ); jiraDownloader.setUseJql( useJql ); jiraDownloader.setWebUser( webUser ); jiraDownloader.setWebPassword( webPassword ); jiraDownloader.setConnectionTimeout( jiraConnectionTimeout ); jiraDownloader.setReceiveTimout( jiraReceiveTimout ); try { jiraDownloader.doExecute(); List<Issue> issueList = jiraDownloader.getIssueList(); if ( StringUtils.isNotEmpty( versionPrefix ) ) { int originalNumberOfIssues = issueList.size(); issueList = IssueUtils.filterIssuesWithVersionPrefix( issueList, versionPrefix ); getLog().debug( "Filtered out " + issueList.size() + " issues of " + originalNumberOfIssues + " that matched the versionPrefix '" + versionPrefix + "'." ); } return getReleases( issueList, new JIRAIssueManagmentSystem() ); } catch ( Exception e ) { throw new MojoExecutionException( "Failed to extract issues from JIRA.", e ); } } private List<Release> getReleases( List<Issue> issues, IssueManagementSystem ims ) throws MojoExecutionException { if ( issueTypes != null ) { ims.applyConfiguration( issueTypes ); } if ( issues.isEmpty() ) { return Collections.emptyList(); } else { IssueAdapter adapter = new IssueAdapter( ims ); return adapter.getReleases( issues ); } } protected List<Release> getTracReleases() throws MojoExecutionException { TracDownloader issueDownloader = new TracDownloader(); issueDownloader.setProject( project ); issueDownloader.setQuery( tracQuery ); issueDownloader.setTracPassword( tracPassword ); issueDownloader.setTracUser( tracUser ); try { return getReleases( issueDownloader.getIssueList(), new TracIssueManagmentSystem() ); } catch ( Exception e ) { throw new MojoExecutionException( "Failed to extract issues from Trac.", e ); } } protected List<Release> getGitHubReleases() throws MojoExecutionException { try { GitHubDownloader issueDownloader = new GitHubDownloader( project, githubAPIScheme, githubAPIPort, includeOpenIssues, true ); issueDownloader.configureAuthentication( settingsDecrypter, githubAPIServerId, settings, getLog() ); return getReleases( issueDownloader.getIssueList(), new GitHubIssueManagementSystem() ); } catch ( Exception e ) { throw new MojoExecutionException( "Failed to extract issues from GitHub.", e ); } } /* * accessors */ public String getArtifactId() { return artifactId; } public void setArtifactId( String artifactId ) { this.artifactId = artifactId; } public String getDevelopmentTeam() { return developmentTeam; } public void setDevelopmentTeam( String developmentTeam ) { this.developmentTeam = developmentTeam; } public String getFinalName() { return finalName; } public void setFinalName( String finalName ) { this.finalName = finalName; } public String getGroupId() { return groupId; } public void setGroupId( String groupId ) { this.groupId = groupId; } public String getIntroduction() { return introduction; } public void setIntroduction( String introduction ) { this.introduction = introduction; } public void setIssueTypes( Map<String, String> issueTypes ) { this.issueTypes = issueTypes; } public Map<String, String> getIssueTypes() { return issueTypes; } public File getAnnouncementDirectory() { return announcementDirectory; } public void setAnnouncementDirectory( File announcementDirectory ) { this.announcementDirectory = announcementDirectory; } public String getPackaging() { return packaging; } public void setPackaging( String packaging ) { this.packaging = packaging; } public String getUrl() { return url; } public void setUrl( String url ) { this.url = url; } public String getUrlDownload() { return urlDownload; } public void setUrlDownload( String urlDownload ) { this.urlDownload = urlDownload; } public VelocityComponent getVelocity() { return velocity; } public void setVelocity( VelocityComponent velocity ) { this.velocity = velocity; } public String getVersion() { return version; } public void setVersion( String version ) { this.version = version; } public ChangesXML getXml() { return xml; } public void setXml( ChangesXML xml ) { this.xml = xml; } public File getXmlPath() { return xmlPath; } public void setXmlPath( File xmlPath ) { this.xmlPath = xmlPath; } }