blob: f005ce49e5ecfae5e14a009b37d357c00f9c8a6a [file] [log] [blame]
package org.apache.maven.plugin.plugin;
/*
* 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.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.siterenderer.Renderer;
import org.apache.maven.execution.RuntimeInformation;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.plugin.descriptor.MNG6109PluginDescriptorBuilder;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.AbstractMavenReportRenderer;
import org.apache.maven.reporting.MavenReportException;
import org.apache.maven.tools.plugin.DefaultPluginToolsRequest;
import org.apache.maven.tools.plugin.PluginToolsRequest;
import org.apache.maven.tools.plugin.extractor.ExtractionException;
import org.apache.maven.tools.plugin.generator.GeneratorException;
import org.apache.maven.tools.plugin.generator.GeneratorUtils;
import org.apache.maven.tools.plugin.generator.PluginXdocGenerator;
import org.apache.maven.tools.plugin.scanner.MojoScanner;
import org.apache.maven.tools.plugin.util.PluginUtils;
import org.codehaus.plexus.component.repository.ComponentDependency;
import org.codehaus.plexus.configuration.PlexusConfigurationException;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.Xpp3Dom;
/**
* Generates the Plugin's documentation report: <code>plugin-info.html</code> plugin overview page,
* and one <code><i>goal</i>-mojo.html</code> per goal.
*
* @author <a href="snicoll@apache.org">Stephane Nicoll</a>
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @since 2.0
*/
@Mojo( name = "report", threadSafe = true )
@Execute( phase = LifecyclePhase.PROCESS_CLASSES )
public class PluginReport
extends AbstractMavenReport
{
/**
* Report output directory for mojos' documentation.
*/
@Parameter( defaultValue = "${project.build.directory}/generated-site/xdoc" )
private File outputDirectory;
/**
* Doxia Site Renderer.
*/
@Component
private Renderer siteRenderer;
/**
* The Maven Project.
*/
@Parameter( defaultValue = "${project}", readonly = true )
private MavenProject project;
/**
* Mojo scanner tools.
*/
@Component
protected MojoScanner mojoScanner;
/**
* The file encoding of the source files.
*
* @since 2.7
*/
@Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
private String encoding;
/**
* Specify some requirements to execute this plugin.
* Example:
* <pre>
* &lt;requirements&gt;
* &lt;maven&gt;2.0&lt;/maven&gt;
* &lt;jdk&gt;1.4&lt;/jdk&gt;
* &lt;memory&gt;256m&lt;/memory&gt;
* &lt;diskSpace&gt;1m&lt;/diskSpace&gt;
* &lt;others&gt;
* &lt;property&gt;
* &lt;name&gt;SVN&lt;/name&gt;
* &lt;value&gt;1.4.6&lt;/value&gt;
* &lt;/property&gt;
* &lt;/others&gt;
* &lt;/requirements&gt;
* </pre>
*
* If not is specified, Maven requirement is extracted from
* <code>&lt;project&gt;&lt;prerequisites&gt;&lt;maven&gt;</code>
* and JDK requirement is extracted from maven-compiler-plugin configuration.
*/
@Parameter
private Requirements requirements;
/**
* The goal prefix that will appear before the ":".
* By default, this plugin applies a heuristic to derive a heuristic from
* the plugin's artifactId.
* <p/>
* It removes any occurrences of the regular expression <strong>-?maven-?</strong>,
* and then removes any occurrences of <strong>-?plugin-?</strong>.
* <p>
* For example, horsefeature-maven-plugin becomes horsefeature.
* </p>
* <p>
* (There is a special case for maven-plugin-plugin: it is mapped to 'plugin')
* </p>
*
* @since 2.4
*/
@Parameter( property = "goalPrefix" )
protected String goalPrefix;
/**
* Set this to "true" to skip invoking any goals or reports of the plugin.
*
* @since 2.8
*/
@Parameter( defaultValue = "false", property = "maven.plugin.skip" )
private boolean skip;
/**
* Set this to "true" to skip generating the report.
*
* @since 2.8
*/
@Parameter( defaultValue = "false", property = "maven.plugin.report.skip" )
private boolean skipReport;
/**
* The set of dependencies for the current project
*
* @since 3.0
*/
@Parameter( defaultValue = "${project.artifacts}", required = true, readonly = true )
protected Set<Artifact> dependencies;
/**
* List of Remote Repositories used by the resolver
*
* @since 3.0
*/
@Parameter( defaultValue = "${project.remoteArtifactRepositories}", required = true, readonly = true )
protected List<ArtifactRepository> remoteRepos;
/**
* Location of the local repository.
*
* @since 3.0
*/
@Parameter( defaultValue = "${localRepository}", required = true, readonly = true )
protected ArtifactRepository local;
/**
* @since 3.5.1
*/
@Component
private RuntimeInformation rtInfo;
/**
* Path to {@code plugin.xml} plugin descriptor to generate the report from.
*
* @since 3.5.1
*/
@Parameter( defaultValue = "${project.build.outputDirectory}/META-INF/maven/plugin.xml", required = true,
readonly = true )
private File pluginXmlFile;
/**
* {@inheritDoc}
*/
@Override
protected Renderer getSiteRenderer()
{
return siteRenderer;
}
/**
* {@inheritDoc}
*/
@Override
protected String getOutputDirectory()
{
// PLUGIN-191: output directory of plugin.html, not *-mojo.xml
return project.getReporting().getOutputDirectory();
}
/**
* {@inheritDoc}
*/
@Override
protected MavenProject getProject()
{
return project;
}
/**
* {@inheritDoc}
*/
@Override
public boolean canGenerateReport()
{
return pluginXmlFile != null && pluginXmlFile.isFile() && pluginXmlFile.canRead();
}
/**
* {@inheritDoc}
*/
@Override
protected void executeReport( Locale locale )
throws MavenReportException
{
if ( !canGenerateReport() )
{
return;
}
if ( skip || skipReport )
{
getLog().info( "Maven Plugin Plugin Report generation skipped." );
return;
}
PluginDescriptor pluginDescriptor = extractPluginDescriptor();
// Generate the mojos' documentation
generateMojosDocumentation( pluginDescriptor, locale );
// Write the overview
PluginOverviewRenderer r =
new PluginOverviewRenderer( project, requirements, getSink(), pluginDescriptor, locale );
r.render();
}
private PluginDescriptor extractPluginDescriptor()
throws MavenReportException
{
PluginDescriptorBuilder builder = getPluginDescriptorBuilder();
try
{
return builder.build( new FileReader( pluginXmlFile ) );
}
catch ( FileNotFoundException | PlexusConfigurationException e )
{
getLog().debug( "Failed to read " + pluginXmlFile + ", fall back to mojoScanner" );
}
// Copy from AbstractGeneratorMojo#execute()
String defaultGoalPrefix = PluginDescriptor.getGoalPrefixFromArtifactId( project.getArtifactId() );
if ( goalPrefix == null )
{
goalPrefix = defaultGoalPrefix;
}
else
{
getLog().warn( "\n\nGoal prefix is specified as: '" + goalPrefix + "'. Maven currently expects it to be '"
+ defaultGoalPrefix + "'.\n" );
}
// TODO: could use this more, eg in the writing of the plugin descriptor!
PluginDescriptor pluginDescriptor = new PluginDescriptor();
pluginDescriptor.setGroupId( project.getGroupId() );
pluginDescriptor.setArtifactId( project.getArtifactId() );
pluginDescriptor.setVersion( project.getVersion() );
pluginDescriptor.setGoalPrefix( goalPrefix );
try
{
List<ComponentDependency> deps = GeneratorUtils.toComponentDependencies( project.getRuntimeDependencies() );
pluginDescriptor.setDependencies( deps );
PluginToolsRequest request = new DefaultPluginToolsRequest( project, pluginDescriptor );
request.setEncoding( encoding );
request.setSkipErrorNoDescriptorsFound( true );
request.setDependencies( dependencies );
request.setLocal( this.local );
request.setRemoteRepos( this.remoteRepos );
try
{
mojoScanner.populatePluginDescriptor( request );
}
catch ( InvalidPluginDescriptorException e )
{
// this is OK, it happens to lifecycle plugins. Allow generation to proceed.
getLog().debug( "Plugin without mojos.", e );
}
}
catch ( ExtractionException e )
{
throw new MavenReportException( "Error extracting plugin descriptor: \'" + e.getLocalizedMessage() + "\'",
e );
}
return pluginDescriptor;
}
/**
* Return the pluginDescriptorBuilder to use based on the Maven version: either use the original from the
* maven-plugin-api or a patched version for Maven versions before the MNG-6109 fix
* (because of Maven MNG-6109 bug that won't give accurate 'since' info when reading plugin.xml).
*
* @return the proper pluginDescriptorBuilder
* @see https://issues.apache.org/jira/browse/MNG-6109
* @see https://issues.apache.org/jira/browse/MPLUGIN-319
*/
private PluginDescriptorBuilder getPluginDescriptorBuilder()
{
PluginDescriptorBuilder pluginDescriptorBuilder;
try
{
VersionRange versionRange = VersionRange.createFromVersionSpec( "(3.3.9,)" );
if ( versionRange.containsVersion( rtInfo.getApplicationVersion() ) )
{
pluginDescriptorBuilder = new PluginDescriptorBuilder();
}
else
{
pluginDescriptorBuilder = new MNG6109PluginDescriptorBuilder();
}
}
catch ( InvalidVersionSpecificationException e )
{
return new MNG6109PluginDescriptorBuilder();
}
return pluginDescriptorBuilder;
}
/**
* {@inheritDoc}
*/
@Override
public String getDescription( Locale locale )
{
return getBundle( locale ).getString( "report.plugin.description" );
}
/**
* {@inheritDoc}
*/
@Override
public String getName( Locale locale )
{
return getBundle( locale ).getString( "report.plugin.name" );
}
/**
* {@inheritDoc}
*/
@Override
public String getOutputName()
{
return "plugin-info";
}
/**
* Generate the mojos documentation, as xdoc files.
*
* @param pluginDescriptor not null
* @param locale not null
* @throws MavenReportException if any
*/
private void generateMojosDocumentation( PluginDescriptor pluginDescriptor, Locale locale )
throws MavenReportException
{
try
{
File outputDir = outputDirectory;
outputDir.mkdirs();
PluginXdocGenerator generator = new PluginXdocGenerator( project, locale );
PluginToolsRequest pluginToolsRequest = new DefaultPluginToolsRequest( project, pluginDescriptor );
generator.execute( outputDir, pluginToolsRequest );
}
catch ( GeneratorException e )
{
throw new MavenReportException( "Error writing plugin documentation", e );
}
}
/**
* @param locale not null
* @return the bundle for this report
*/
protected static ResourceBundle getBundle( Locale locale )
{
return ResourceBundle.getBundle( "plugin-report", locale, PluginReport.class.getClassLoader() );
}
/**
* Generates an overview page with the list of goals
* and a link to the goal's page.
*/
static class PluginOverviewRenderer
extends AbstractMavenReportRenderer
{
private final MavenProject project;
private final Requirements requirements;
private final PluginDescriptor pluginDescriptor;
private final Locale locale;
/**
* @param project not null
* @param requirements not null
* @param sink not null
* @param pluginDescriptor not null
* @param locale not null
*/
PluginOverviewRenderer( MavenProject project, Requirements requirements, Sink sink,
PluginDescriptor pluginDescriptor, Locale locale )
{
super( sink );
this.project = project;
this.requirements = ( requirements == null ? new Requirements() : requirements );
this.pluginDescriptor = pluginDescriptor;
this.locale = locale;
}
/**
* {@inheritDoc}
*/
@Override
public String getTitle()
{
return getBundle( locale ).getString( "report.plugin.title" );
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings( { "unchecked", "rawtypes" } )
public void renderBody()
{
startSection( getTitle() );
if ( !( pluginDescriptor.getMojos() != null && pluginDescriptor.getMojos().size() > 0 ) )
{
paragraph( getBundle( locale ).getString( "report.plugin.goals.nogoal" ) );
endSection();
return;
}
paragraph( getBundle( locale ).getString( "report.plugin.goals.intro" ) );
boolean hasMavenReport = false;
for ( Iterator<MojoDescriptor> i = pluginDescriptor.getMojos().iterator(); i.hasNext(); )
{
MojoDescriptor mojo = i.next();
if ( GeneratorUtils.isMavenReport( mojo.getImplementation(), project ) )
{
hasMavenReport = true;
}
}
startTable();
String goalColumnName = getBundle( locale ).getString( "report.plugin.goals.column.goal" );
String isMavenReport = getBundle( locale ).getString( "report.plugin.goals.column.isMavenReport" );
String descriptionColumnName = getBundle( locale ).getString( "report.plugin.goals.column.description" );
if ( hasMavenReport )
{
tableHeader( new String[]{ goalColumnName, isMavenReport, descriptionColumnName } );
}
else
{
tableHeader( new String[]{ goalColumnName, descriptionColumnName } );
}
List<MojoDescriptor> mojos = new ArrayList<>();
mojos.addAll( pluginDescriptor.getMojos() );
PluginUtils.sortMojos( mojos );
for ( MojoDescriptor mojo : mojos )
{
String goalName = mojo.getFullGoalName();
/*
* Added ./ to define a relative path
* @see AbstractMavenReportRenderer#getValidHref(java.lang.String)
*/
String goalDocumentationLink = "./" + mojo.getGoal() + "-mojo.html";
String description;
if ( StringUtils.isNotEmpty( mojo.getDeprecated() ) )
{
description =
"<strong>" + getBundle( locale ).getString( "report.plugin.goal.deprecated" ) + "</strong> "
+ GeneratorUtils.makeHtmlValid( mojo.getDeprecated() );
}
else if ( StringUtils.isNotEmpty( mojo.getDescription() ) )
{
description = GeneratorUtils.makeHtmlValid( mojo.getDescription() );
}
else
{
description = getBundle( locale ).getString( "report.plugin.goal.nodescription" );
}
sink.tableRow();
tableCell( createLinkPatternedText( goalName, goalDocumentationLink ) );
if ( hasMavenReport )
{
if ( GeneratorUtils.isMavenReport( mojo.getImplementation(), project ) )
{
sink.tableCell();
sink.text( getBundle( locale ).getString( "report.plugin.isReport" ) );
sink.tableCell_();
}
else
{
sink.tableCell();
sink.text( getBundle( locale ).getString( "report.plugin.isNotReport" ) );
sink.tableCell_();
}
}
tableCell( description, true );
sink.tableRow_();
}
endTable();
startSection( getBundle( locale ).getString( "report.plugin.systemrequirements" ) );
paragraph( getBundle( locale ).getString( "report.plugin.systemrequirements.intro" ) );
startTable();
String maven = discoverMavenRequirement( project, requirements );
sink.tableRow();
tableCell( getBundle( locale ).getString( "report.plugin.systemrequirements.maven" ) );
tableCell( ( maven != null
? maven
: getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
sink.tableRow_();
String jdk = discoverJdkRequirement( project, requirements );
sink.tableRow();
tableCell( getBundle( locale ).getString( "report.plugin.systemrequirements.jdk" ) );
tableCell(
( jdk != null ? jdk : getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
sink.tableRow_();
sink.tableRow();
tableCell( getBundle( locale ).getString( "report.plugin.systemrequirements.memory" ) );
tableCell( ( StringUtils.isNotEmpty( requirements.getMemory() )
? requirements.getMemory()
: getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
sink.tableRow_();
sink.tableRow();
tableCell( getBundle( locale ).getString( "report.plugin.systemrequirements.diskspace" ) );
tableCell( ( StringUtils.isNotEmpty( requirements.getDiskSpace() )
? requirements.getDiskSpace()
: getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
sink.tableRow_();
if ( requirements.getOthers() != null && requirements.getOthers().size() > 0 )
{
for ( Iterator it = requirements.getOthers().keySet().iterator(); it.hasNext(); )
{
String key = it.next().toString();
sink.tableRow();
tableCell( key );
tableCell( ( StringUtils.isNotEmpty( requirements.getOthers().getProperty( key ) )
? requirements.getOthers().getProperty( key )
: getBundle( locale ).getString( "report.plugin.systemrequirements.nominimum" ) ) );
sink.tableRow_();
}
}
endTable();
endSection();
renderUsageSection( hasMavenReport );
endSection();
}
/**
* Render the section about the usage of the plugin.
*
* @param hasMavenReport If the plugin has a report or not
*/
private void renderUsageSection( boolean hasMavenReport )
{
startSection( getBundle( locale ).getString( "report.plugin.usage" ) );
// Configuration
sink.paragraph();
text( getBundle( locale ).getString( "report.plugin.usage.intro" ) );
sink.paragraph_();
StringBuilder sb = new StringBuilder();
sb.append( "<project>" ).append( '\n' );
sb.append( " ..." ).append( '\n' );
sb.append( " <build>" ).append( '\n' );
sb.append(
" <!-- " + getBundle( locale ).getString( "report.plugin.usage.pluginManagement" ) + " -->" ).append(
'\n' );
sb.append( " <pluginManagement>" ).append( '\n' );
sb.append( " <plugins>" ).append( '\n' );
sb.append( " <plugin>" ).append( '\n' );
sb.append( " <groupId>" ).append( pluginDescriptor.getGroupId() ).append( "</groupId>" ).append(
'\n' );
sb.append( " <artifactId>" ).append( pluginDescriptor.getArtifactId() ).append(
"</artifactId>" ).append( '\n' );
sb.append( " <version>" ).append( pluginDescriptor.getVersion() ).append( "</version>" ).append(
'\n' );
sb.append( " </plugin>" ).append( '\n' );
sb.append( " ..." ).append( '\n' );
sb.append( " </plugins>" ).append( '\n' );
sb.append( " </pluginManagement>" ).append( '\n' );
sb.append( " <!-- " + getBundle( locale ).getString( "report.plugin.usage.plugins" ) + " -->" ).append(
'\n' );
sb.append( " <plugins>" ).append( '\n' );
sb.append( " <plugin>" ).append( '\n' );
sb.append( " <groupId>" ).append( pluginDescriptor.getGroupId() ).append( "</groupId>" ).append(
'\n' );
sb.append( " <artifactId>" ).append( pluginDescriptor.getArtifactId() ).append(
"</artifactId>" ).append( '\n' );
sb.append( " <version>" ).append( pluginDescriptor.getVersion() ).append( "</version>" ).append(
'\n' );
sb.append( " </plugin>" ).append( '\n' );
sb.append( " ..." ).append( '\n' );
sb.append( " </plugins>" ).append( '\n' );
sb.append( " </build>" ).append( '\n' );
if ( hasMavenReport )
{
sb.append( " ..." ).append( '\n' );
sb.append(
" <!-- " + getBundle( locale ).getString( "report.plugin.usage.reporting" ) + " -->" ).append(
'\n' );
sb.append( " <reporting>" ).append( '\n' );
sb.append( " <plugins>" ).append( '\n' );
sb.append( " <plugin>" ).append( '\n' );
sb.append( " <groupId>" ).append( pluginDescriptor.getGroupId() ).append( "</groupId>" ).append(
'\n' );
sb.append( " <artifactId>" ).append( pluginDescriptor.getArtifactId() ).append(
"</artifactId>" ).append( '\n' );
sb.append( " <version>" ).append( pluginDescriptor.getVersion() ).append( "</version>" ).append(
'\n' );
sb.append( " </plugin>" ).append( '\n' );
sb.append( " ..." ).append( '\n' );
sb.append( " </plugins>" ).append( '\n' );
sb.append( " </reporting>" ).append( '\n' );
}
sb.append( " ..." ).append( '\n' );
sb.append( "</project>" ).append( '\n' );
verbatimText( sb.toString() );
sink.paragraph();
linkPatternedText( getBundle( locale ).getString( "report.plugin.configuration.end" ) );
sink.paragraph_();
endSection();
}
/**
* Try to lookup on the Maven prerequisites property.
* If not specified, uses the value defined by the user.
*
* @param project not null
* @param requirements not null
* @return the Maven version
*/
private static String discoverMavenRequirement( MavenProject project, Requirements requirements )
{
String maven = requirements.getMaven();
if ( maven == null )
{
maven = ( project.getPrerequisites() != null ? project.getPrerequisites().getMaven() : null );
}
if ( maven == null )
{
maven = "2.0";
}
return maven;
}
/**
* <ol>
* <li>use configured jdk requirement</li>
* <li>use <code>target</code> configuration of <code>org.apache.maven.plugins:maven-compiler-plugin</code></li>
* <li>use <code>target</code> configuration of <code>org.apache.maven.plugins:maven-compiler-plugin</code> in
* <code>pluginManagement</code></li>
* <li>use <code>maven.compiler.target</code> property</li>
* </ol>
*
* @param project not null
* @param requirements not null
* @return the JDK version
*/
private static String discoverJdkRequirement( MavenProject project, Requirements requirements )
{
String jdk = requirements.getJdk();
if ( jdk != null )
{
return jdk;
}
Plugin compiler = getCompilerPlugin( project.getBuild().getPluginsAsMap() );
if ( compiler == null )
{
compiler = getCompilerPlugin( project.getPluginManagement().getPluginsAsMap() );
}
jdk = getPluginParameter( compiler, "target" );
if ( jdk != null )
{
return jdk;
}
jdk = getPluginParameter( compiler, "release" );
if ( jdk != null )
{
return jdk;
}
// default value
jdk = project.getProperties().getProperty( "maven.compiler.target" );
if ( jdk != null )
{
return jdk;
}
// return "1.5" by default?
String version = ( compiler == null ) ? null : compiler.getVersion();
if ( version != null )
{
return "Default target for maven-compiler-plugin version " + version;
}
return "Unknown";
}
private static Plugin getCompilerPlugin( Map<String, Plugin> pluginsAsMap )
{
return pluginsAsMap.get( "org.apache.maven.plugins:maven-compiler-plugin" );
}
private static String getPluginParameter( Plugin plugin, String parameter )
{
if ( plugin != null )
{
Xpp3Dom pluginConf = (Xpp3Dom) plugin.getConfiguration();
if ( pluginConf != null )
{
Xpp3Dom target = pluginConf.getChild( parameter );
if ( target != null )
{
return target.getValue();
}
}
}
return null;
}
}
}