| package org.apache.maven.plugin.changes; |
| |
| /* |
| * 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.FileWriter; |
| import java.io.IOException; |
| import java.io.Writer; |
| |
| import java.net.URL; |
| |
| import java.text.SimpleDateFormat; |
| |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.ResourceBundle; |
| |
| import org.apache.commons.collections.map.CaseInsensitiveMap; |
| import org.apache.maven.execution.MavenSession; |
| 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.project.MavenProject; |
| import org.apache.maven.reporting.MavenReportException; |
| import org.apache.maven.shared.filtering.MavenFileFilter; |
| import org.apache.maven.shared.filtering.MavenFileFilterRequest; |
| import org.apache.maven.shared.filtering.MavenFilteringException; |
| |
| import org.codehaus.plexus.util.FileUtils; |
| import org.codehaus.plexus.util.IOUtil; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.apache.commons.io.input.XmlStreamReader; |
| |
| /** |
| * Goal which creates a nicely formatted Changes Report in html format from a changes.xml file. |
| * |
| * @author <a href="mailto:jruiz@exist.com">Johnny R. Ruiz III</a> |
| * @version $Id$ |
| */ |
| @Mojo( name = "changes-report", threadSafe = true ) |
| public class ChangesMojo |
| extends AbstractChangesReport |
| { |
| /** |
| * A flag whether the report should also include changes from child modules. If set to <code>false</code>, only |
| * the changes from current project will be written to the report. |
| * |
| * @since 2.5 |
| */ |
| @Parameter( defaultValue = "false" ) |
| private boolean aggregated; |
| |
| /** |
| * A flag whether the report should also include the dates of individual actions. If set to <code>false</code>, only |
| * the dates of releases will be written to the report. |
| * |
| * @since 2.1 |
| */ |
| @Parameter( property = "changes.addActionDate", defaultValue = "false" ) |
| private boolean addActionDate; |
| |
| /** |
| * Whether HTML code within an action should be escaped. By changing this to |
| * <code>false</code> you can restore the behavior that was in version 2.2 |
| * of this plugin, allowing you to use HTML code to format the content of an |
| * action. |
| * <p> |
| * <strong>Note:</strong> If you use HTML code in an action you need to |
| * place it inside a CDATA section. |
| * </p> |
| * <strong>Note:</strong> Putting any kind of markup inside a CDATA section |
| * might mess up the Changes Report or other generated documents, such as |
| * PDFs, that are based on your <code>changes.xml</code> file if you are not |
| * careful. |
| * |
| * @since 2.4 |
| * @deprecated using markup inside CDATA sections does not work for all output formats! |
| */ |
| @Parameter( defaultValue = "true" ) |
| private boolean escapeHTML; |
| |
| /** |
| * The directory for interpolated changes.xml. |
| * |
| * @since 2.2 |
| */ |
| @Parameter( defaultValue = "${project.build.directory}/changes", required = true, readonly = true ) |
| private File filteredOutputDirectory; |
| |
| /** |
| * applying filtering filtering "a la" resources plugin |
| * |
| * @since 2.2 |
| */ |
| @Parameter( defaultValue = "false" ) |
| private boolean filteringChanges; |
| |
| /** |
| * Template string that is used to discover the URL to use to display an issue report. |
| * There are 2 template tokens you can use. <code>%URL%</code>: this is computed by getting the |
| * <code><issueManagement>/<url></code> value from the POM, and removing the last '/' |
| * and everything that comes after it. <code>%ISSUE%</code>: this is the issue number. |
| * <p> |
| * <strong>Note:</strong> In versions of this plugin prior to 2.0-beta-2 this parameter was called |
| * <code>link_template</code>. |
| * </p> |
| * |
| * @since 2.0-beta-2 |
| * @deprecated As of 2.1 use issueLinkTemplatePerSystem : this one will be with system default |
| */ |
| @Parameter( property = "changes.issueLinkTemplate", defaultValue = "%URL%/ViewIssue.jspa?key=%ISSUE%" ) |
| private String issueLinkTemplate; |
| |
| /** |
| * Template strings per system that is used to discover the URL to use to display an issue report. Each key in this |
| * map denotes the (case-insensitive) identifier of the issue tracking system and its value gives the URL template. |
| * <p> |
| * There are 2 template tokens you can use. <code>%URL%</code>: this is computed by getting the |
| * <code><issueManagement>/<url></code> value from the POM, and removing the last '/' |
| * and everything that comes after it. <code>%ISSUE%</code>: this is the issue number. |
| * </p> |
| * <p> |
| * <strong>Note:</strong> The deprecated issueLinkTemplate will be used for a system called "default". |
| * </p> |
| * <p> |
| * <strong>Note:</strong> Starting with version 2.4 you usually don't need |
| * to specify this, unless you need to link to an issue management system in |
| * your Changes report that isn't supported out of the box. See the |
| * <a href="./usage.html">Usage page</a> for more |
| * information. |
| * </p> |
| * |
| * @since 2.1 |
| */ |
| @Parameter |
| private Map issueLinkTemplatePerSystem; |
| |
| /** |
| * @since 2.2 |
| */ |
| @Component |
| private MavenFileFilter mavenFileFilter; |
| |
| /** |
| * Format to use for publishDate. The value will be available with the following expression ${publishDate} |
| * |
| * @see java.text.SimpleDateFormat |
| * @since 2.2 |
| */ |
| @Parameter( defaultValue = "yyyy-MM-dd" ) |
| private String publishDateFormat; |
| |
| /** |
| * Locale to use for publishDate when formatting |
| * |
| * @see java.util.Locale |
| * @since 2.2 |
| */ |
| @Parameter( defaultValue = "en" ) |
| private String publishDateLocale; |
| |
| /** |
| * @since 2.2 |
| */ |
| @Component |
| protected MavenSession session; |
| |
| /** |
| * @since 2.4 |
| */ |
| @Parameter( defaultValue = "${project.issueManagement.system}", readonly = true ) |
| private String system; |
| |
| /** |
| * The URI of a file containing all the team members. If this is set to the |
| * special value "none", no links will be generated for the team members. |
| * |
| * @since 2.4 |
| */ |
| @Parameter( defaultValue = "team-list.html" ) |
| private String teamlist; |
| |
| /** |
| */ |
| @Parameter( defaultValue = "${project.issueManagement.url}", readonly = true ) |
| private String url; |
| |
| /** |
| * The type of the feed to generate. |
| * |
| * <p> |
| * Supported values are: |
| * <code>"rss_0.9", "rss_0.91N" (RSS 0.91 Netscape), "rss_0.91U" (RSS 0.91 Userland), |
| * "rss_0.92", "rss_0.93", "rss_0.94", "rss_1.0", "rss_2.0", "atom_0.3", "atom_1.0"</code>. |
| * </p> |
| * |
| * <p>If not specified, no feed is generated.</p> |
| * |
| * @since 2.9 |
| */ |
| @Parameter |
| private String feedType; |
| |
| /** |
| * The path of the <code>changes.xml</code> file that will be converted into an HTML report. |
| */ |
| @Parameter( property = "changes.xmlPath", defaultValue = "src/changes/changes.xml" ) |
| private File xmlPath; |
| |
| private ReleaseUtils releaseUtils = new ReleaseUtils( getLog() ); |
| |
| private CaseInsensitiveMap caseInsensitiveIssueLinkTemplatePerSystem; |
| |
| /* --------------------------------------------------------------------- */ |
| /* Public methods */ |
| /* --------------------------------------------------------------------- */ |
| |
| public boolean canGenerateReport() |
| { |
| return xmlPath.isFile(); |
| } |
| |
| public void executeReport( Locale locale ) |
| throws MavenReportException |
| { |
| Date now = new Date(); |
| SimpleDateFormat simpleDateFormat = |
| new SimpleDateFormat(publishDateFormat, new Locale(publishDateLocale)); |
| Properties additionalProperties = new Properties(); |
| additionalProperties.put("publishDate", simpleDateFormat.format(now)); |
| |
| ChangesXML changesXml = getChangesFromFile( xmlPath, project, additionalProperties); |
| if ( changesXml == null ) return; |
| |
| if ( aggregated ) |
| { |
| final String basePath = project.getBasedir().getAbsolutePath(); |
| final String absolutePath = xmlPath.getAbsolutePath(); |
| if ( !absolutePath.startsWith( basePath ) ) |
| { |
| getLog().warn( "xmlPath should be within the project dir for aggregated changes report." ); |
| return; |
| } |
| final String relativePath = absolutePath.substring( basePath.length() ); |
| |
| List releaseList = changesXml.getReleaseList(); |
| for (Object o : project.getCollectedProjects()) { |
| final MavenProject childProject = (MavenProject) o; |
| final File changesFile = new File(childProject.getBasedir(), relativePath); |
| final ChangesXML childXml = getChangesFromFile(changesFile, childProject, additionalProperties); |
| if (childXml != null) { |
| releaseList = releaseUtils.mergeReleases(releaseList, childProject.getName(), childXml.getReleaseList()); |
| } |
| } |
| changesXml.setReleaseList( releaseList ); |
| } |
| |
| ChangesReportGenerator report = new ChangesReportGenerator( changesXml.getReleaseList() ); |
| |
| report.setAuthor( changesXml.getAuthor() ); |
| report.setTitle( changesXml.getTitle() ); |
| |
| report.setEscapeHTML ( escapeHTML ); |
| |
| // Create a case insensitive version of issueLinkTemplatePerSystem |
| // We need something case insensitive to maintain backward compatibility |
| if ( issueLinkTemplatePerSystem == null ) |
| { |
| caseInsensitiveIssueLinkTemplatePerSystem = new CaseInsensitiveMap(); |
| } |
| else |
| { |
| caseInsensitiveIssueLinkTemplatePerSystem = new CaseInsensitiveMap( issueLinkTemplatePerSystem ); |
| } |
| |
| // Set good default values for issue management systems here, but only |
| // if they have not been configured already by the user |
| addIssueLinkTemplate( ChangesReportGenerator.DEFAULT_ISSUE_SYSTEM_KEY, issueLinkTemplate ); |
| addIssueLinkTemplate( "Bitbucket", "%URL%/issue/%ISSUE%" ); |
| addIssueLinkTemplate( "Bugzilla", "%URL%/show_bug.cgi?id=%ISSUE%" ); |
| addIssueLinkTemplate( "GitHub", "%URL%/%ISSUE%" ); |
| addIssueLinkTemplate( "GoogleCode", "%URL%/detail?id=%ISSUE%" ); |
| addIssueLinkTemplate( "JIRA", "%URL%/%ISSUE%" ); |
| addIssueLinkTemplate( "Mantis", "%URL%/view.php?id=%ISSUE%" ); |
| addIssueLinkTemplate( "MKS", "%URL%/viewissue?selection=%ISSUE%" ); |
| addIssueLinkTemplate( "Redmine", "%URL%/issues/show/%ISSUE%" ); |
| addIssueLinkTemplate( "Scarab", "%URL%/issues/id/%ISSUE%" ); |
| addIssueLinkTemplate( "SourceForge", "http://sourceforge.net/support/tracker.php?aid=%ISSUE%" ); |
| addIssueLinkTemplate( "SourceForge2", "%URL%/%ISSUE%" ); |
| addIssueLinkTemplate( "Trac", "%URL%/ticket/%ISSUE%" ); |
| addIssueLinkTemplate( "Trackplus", "%URL%/printItem.action?key=%ISSUE%" ); |
| addIssueLinkTemplate( "YouTrack", "%URL%/issue/%ISSUE%" ); |
| // @todo Add more issue management systems here |
| // Remember to also add documentation in usage.apt.vm |
| |
| // Show the current issueLinkTemplatePerSystem configuration |
| logIssueLinkTemplatePerSystem( caseInsensitiveIssueLinkTemplatePerSystem ); |
| |
| report.setIssueLinksPerSystem( caseInsensitiveIssueLinkTemplatePerSystem ); |
| |
| report.setSystem( system ); |
| |
| report.setTeamlist ( teamlist ); |
| |
| report.setUrl( url ); |
| |
| report.setAddActionDate( addActionDate ); |
| |
| if ( StringUtils.isEmpty( url ) ) |
| { |
| getLog().warn( "No issue management URL defined in POM. Links to your issues will not work correctly." ); |
| } |
| |
| boolean feedGenerated = false; |
| |
| if ( StringUtils.isNotEmpty( feedType ) ) |
| { |
| feedGenerated = generateFeed( changesXml, locale ); |
| } |
| |
| report.setLinkToFeed( feedGenerated ); |
| |
| report.doGenerateReport( getBundle( locale ), getSink() ); |
| |
| // Copy the images |
| copyStaticResources(); |
| } |
| |
| public String getDescription( Locale locale ) |
| { |
| return getBundle( locale ).getString( "report.issues.description" ); |
| } |
| |
| public String getName( Locale locale ) |
| { |
| return getBundle( locale ).getString( "report.issues.name" ); |
| } |
| |
| public String getOutputName() |
| { |
| return "changes-report"; |
| } |
| |
| /* --------------------------------------------------------------------- */ |
| /* Private methods */ |
| /* --------------------------------------------------------------------- */ |
| |
| /** |
| * Parses specified changes.xml file. It also makes filtering if needed. If specified file doesn't exist |
| * it will log warning and return <code>null</code>. |
| * |
| * @param changesXml changes xml file to parse |
| * @param project maven project to parse changes for |
| * @param additionalProperties additional properties used for filtering |
| * @return parsed <code>ChangesXML</code> instance or null if file doesn't exist |
| * @throws MavenReportException if any errors occurs while parsing |
| */ |
| private ChangesXML getChangesFromFile( File changesXml, MavenProject project, Properties additionalProperties ) |
| throws MavenReportException |
| { |
| if ( !changesXml.exists() ) |
| { |
| getLog().warn( "changes.xml file " + changesXml.getAbsolutePath() + " does not exist." ); |
| return null; |
| } |
| |
| if ( filteringChanges ) |
| { |
| if ( !filteredOutputDirectory.exists() ) |
| { |
| filteredOutputDirectory.mkdirs(); |
| } |
| XmlStreamReader xmlStreamReader = null; |
| try |
| { |
| // so we get encoding from the file itself |
| xmlStreamReader = new XmlStreamReader( changesXml ); |
| String encoding = xmlStreamReader.getEncoding(); |
| File resultFile = new File( filteredOutputDirectory, project.getGroupId() + "." + project.getArtifactId() + "-changes.xml" ); |
| |
| final MavenFileFilterRequest mavenFileFilterRequest = |
| new MavenFileFilterRequest( changesXml, resultFile, true, project, Collections.EMPTY_LIST, false, |
| encoding, session, additionalProperties ); |
| mavenFileFilter.copyFile( mavenFileFilterRequest ); |
| changesXml = resultFile; |
| } |
| catch ( IOException e ) |
| { |
| throw new MavenReportException( "Exception during filtering changes file : " + e.getMessage(), e ); |
| } |
| catch ( MavenFilteringException e ) |
| { |
| throw new MavenReportException( "Exception during filtering changes file : " + e.getMessage(), e ); |
| } |
| finally |
| { |
| if ( xmlStreamReader != null ) |
| { |
| IOUtil.close( xmlStreamReader ); |
| } |
| } |
| |
| } |
| return new ChangesXML( changesXml, getLog() ); |
| } |
| |
| /** |
| * Add the issue link template for the given issue management system, |
| * but only if it has not already been configured. |
| * |
| * @param system The issue management system |
| * @param issueLinkTemplate The issue link template to use |
| * @since 2.4 |
| */ |
| private void addIssueLinkTemplate( String system, String issueLinkTemplate ) |
| { |
| if ( caseInsensitiveIssueLinkTemplatePerSystem == null ) |
| { |
| caseInsensitiveIssueLinkTemplatePerSystem = new CaseInsensitiveMap(); |
| } |
| if ( !caseInsensitiveIssueLinkTemplatePerSystem.containsKey( system ) ) |
| { |
| caseInsensitiveIssueLinkTemplatePerSystem.put( system, issueLinkTemplate ); |
| } |
| } |
| |
| private void copyStaticResources() |
| throws MavenReportException |
| { |
| final String pluginResourcesBase = "org/apache/maven/plugin/changes"; |
| String resourceNames[] = { |
| "images/add.gif", |
| "images/fix.gif", |
| "images/icon_help_sml.gif", |
| "images/remove.gif", |
| "images/rss.png", |
| "images/update.gif" }; |
| try |
| { |
| getLog().debug( "Copying static resources." ); |
| for (String resourceName : resourceNames) { |
| URL url = this.getClass().getClassLoader().getResource(pluginResourcesBase + "/" + resourceName); |
| FileUtils.copyURLToFile(url, new File(getReportOutputDirectory(), resourceName)); |
| } |
| } |
| catch ( IOException e ) |
| { |
| throw new MavenReportException( "Unable to copy static resources." ); |
| } |
| } |
| |
| private ResourceBundle getBundle( Locale locale ) |
| { |
| return ResourceBundle.getBundle( "changes-report", locale, this.getClass().getClassLoader() ); |
| } |
| |
| protected String getTeamlist() |
| { |
| return teamlist; |
| } |
| |
| private void logIssueLinkTemplatePerSystem( Map issueLinkTemplatePerSystem ) |
| { |
| if ( getLog().isDebugEnabled() ) |
| { |
| if ( issueLinkTemplatePerSystem == null ) |
| { |
| getLog().debug( "No issueLinkTemplatePerSystem configuration was found" ); |
| } |
| else |
| { |
| for (Object o : issueLinkTemplatePerSystem.entrySet()) { |
| Map.Entry entry = (Map.Entry) o; |
| getLog().debug("issueLinkTemplatePerSystem[" + entry.getKey() + "] = " + entry.getValue()); |
| } |
| } |
| } |
| } |
| |
| private boolean generateFeed( final ChangesXML changesXml, final Locale locale ) |
| { |
| getLog().debug( "Generating " + feedType + " feed." ); |
| |
| boolean success = true; |
| |
| final FeedGenerator feed = new FeedGenerator( locale ); |
| feed.setLink( project.getUrl() + "/changes-report.html" ); // TODO: better way? |
| feed.setTitle( project.getName() + ": " + changesXml.getTitle() ); |
| feed.setAuthor( changesXml.getAuthor() ); |
| feed.setDateFormat( new SimpleDateFormat( publishDateFormat, new Locale( publishDateLocale ) ) ); |
| |
| Writer writer = null; |
| |
| try |
| { |
| writer = new FileWriter( new File( getReportOutputDirectory(), "changes.rss" ) ); |
| feed.export( changesXml.getReleaseList(), feedType, writer ); |
| } |
| catch ( IOException ex ) |
| { |
| success = false; |
| getLog().warn( "Failed to create rss feed: " + ex.getMessage() ); |
| getLog().debug( ex ); |
| } |
| finally |
| { |
| try |
| { |
| if ( writer != null ) |
| { |
| writer.close(); |
| } |
| } |
| catch ( IOException ex ) |
| { |
| getLog().warn( "Failed to close writer: " + ex.getMessage() ); |
| getLog().debug( ex ); |
| } |
| } |
| |
| return success; |
| } |
| } |