package org.apache.maven.reporting.exec; /* * 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.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.maven.lifecycle.LifecycleExecutor; import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MavenPluginManager; import org.apache.maven.plugin.Mojo; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoNotFoundException; import org.apache.maven.plugin.PluginConfigurationException; import org.apache.maven.plugin.PluginContainerException; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugin.version.DefaultPluginVersionRequest; import org.apache.maven.plugin.version.PluginVersionRequest; import org.apache.maven.plugin.version.PluginVersionResolutionException; import org.apache.maven.plugin.version.PluginVersionResolver; import org.apache.maven.plugin.version.PluginVersionResult; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.MavenReport; import org.codehaus.plexus.component.annotations.Component; import org.codehaus.plexus.component.annotations.Requirement; import org.codehaus.plexus.configuration.PlexusConfiguration; import org.codehaus.plexus.logging.Logger; import org.apache.maven.shared.utils.StringUtils; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.codehaus.plexus.util.xml.Xpp3DomUtils; /** * <p> * This component will build some {@link MavenReportExecution} from {@link MavenReportExecutorRequest}. If a * {@link MavenReport} needs to fork a lifecycle, this fork is executed here. It will ask the core to get some * informations in order to correctly setup {@link MavenReport}. * </p> * <p> * <b>Note</b> if no version is defined in the report plugin, the version will be searched with * {@link #resolvePluginVersion(ReportPlugin, MavenReportExecutorRequest) resolvePluginVersion(...)} method: * <ol> * <li>use the one defined in the reportPlugin configuration,</li> * <li>search similar (same groupId and artifactId) plugin in the build/plugins section of the pom,</li> * <li>search similar (same groupId and artifactId) plugin in the build/pluginManagement section of the pom,</li> * <li>ask {@link PluginVersionResolver} to get a fallback version (display a warning as it's not a recommended use). * </li> * </ol> * </p> * <p> * Following steps are done: * <ul> * <li>get {@link PluginDescriptor} from the {@link MavenPluginManager} (through * {@link MavenPluginManagerHelper#getPluginDescriptor(Plugin, org.apache.maven.execution.MavenSession) * MavenPluginManagerHelper.getPluginDescriptor(...)} to protect from core API change)</li> * <li>setup a {@link ClassLoader}, with the Site plugin classloader as parent for the report execution. <br> * Notice that some classes are imported from the current Site plugin ClassRealm: see {@link #IMPORTS}. Corresponding * artifacts are excluded from the artifact resolution: <code>doxia-site-renderer</code>, <code>doxia-sink-api</code> *  and <code>maven-reporting-api</code>.<br> * Work is done using {@link MavenPluginManager} (through * {@link MavenPluginManagerHelper#setupPluginRealm(PluginDescriptor, MavenSession, ClassLoader, List, List) * MavenPluginManagerHelper.setupPluginRealm(...)} to protect from core API change)</li> * <li>setup the mojo using {@link MavenPluginManager#getConfiguredMojo(Class, MavenSession, MojoExecution) * MavenPluginManager.getConfiguredMojo(...)}</li> * <li>verify with {@link LifecycleExecutor#calculateForkedExecutions(MojoExecution, MavenSession) * LifecycleExecutor.calculateForkedExecutions(...)} if any forked execution is needed: if yes, execute the forked * execution here</li> * </ul> * </p> * * @author Olivier Lamy */ @Component( role = MavenReportExecutor.class ) public class DefaultMavenReportExecutor implements MavenReportExecutor { @Requirement private Logger logger; @Requirement protected MavenPluginManager mavenPluginManager; @Requirement protected MavenPluginManagerHelper mavenPluginManagerHelper; @Requirement protected LifecycleExecutor lifecycleExecutor; @Requirement protected PluginVersionResolver pluginVersionResolver; private static final List<String> IMPORTS = Arrays.asList( "org.apache.maven.reporting.MavenReport", "org.apache.maven.reporting.MavenMultiPageReport", "org.apache.maven.doxia.siterenderer.Renderer", "org.apache.maven.doxia.sink.SinkFactory", "org.codehaus.doxia.sink.Sink", "org.apache.maven.doxia.sink.Sink", "org.apache.maven.doxia.sink.SinkEventAttributes", "org.apache.maven.doxia.logging.LogEnabled", "org.apache.maven.doxia.logging.Log" ); private static final List<String> EXCLUDES = Arrays.asList( "doxia-site-renderer", "doxia-sink-api", "maven-reporting-api" ); public List<MavenReportExecution> buildMavenReports( MavenReportExecutorRequest mavenReportExecutorRequest ) throws MojoExecutionException { if ( mavenReportExecutorRequest.getReportPlugins() == null ) { return Collections.emptyList(); } getLog().debug( "DefaultMavenReportExecutor.buildMavenReports()" ); Set<String> reportPluginKeys = new HashSet<String>(); List<MavenReportExecution> reportExecutions = new ArrayList<MavenReportExecution>(); String pluginKey = ""; try { for ( ReportPlugin reportPlugin : mavenReportExecutorRequest.getReportPlugins() ) { pluginKey = reportPlugin.getGroupId() + ':' + reportPlugin.getArtifactId(); if ( !reportPluginKeys.add( pluginKey ) ) { logger.info( "plugin " + pluginKey + " will be executed more than one time" ); } reportExecutions.addAll( buildReportPlugin( mavenReportExecutorRequest, reportPlugin ) ); } } catch ( Exception e ) { throw new MojoExecutionException( "failed to get report for " + pluginKey, e ); } return reportExecutions; } protected List<MavenReportExecution> buildReportPlugin( MavenReportExecutorRequest mavenReportExecutorRequest, ReportPlugin reportPlugin ) throws Exception { // step 1: prepare the plugin Plugin plugin = new Plugin(); plugin.setGroupId( reportPlugin.getGroupId() ); plugin.setArtifactId( reportPlugin.getArtifactId() ); plugin.setVersion( resolvePluginVersion( reportPlugin, mavenReportExecutorRequest ) ); logger.info( "configuring report plugin " + plugin.getId() ); mergePluginToReportPlugin( mavenReportExecutorRequest, plugin, reportPlugin ); PluginDescriptor pluginDescriptor = mavenPluginManagerHelper.getPluginDescriptor( plugin, mavenReportExecutorRequest.getMavenSession() ); // step 2: prepare the goals List<GoalWithConf> goalsWithConfiguration = new ArrayList<GoalWithConf>(); boolean hasUserDefinedReports = prepareGoals( reportPlugin, pluginDescriptor, goalsWithConfiguration ); // step 3: prepare the reports List<MavenReportExecution> reports = new ArrayList<MavenReportExecution>(); for ( GoalWithConf report : goalsWithConfiguration ) { MavenReportExecution mavenReportExecution = prepareReportExecution( mavenReportExecutorRequest, report, hasUserDefinedReports ); if ( mavenReportExecution != null ) { // ok, report is ready to generate reports.add( mavenReportExecution ); } } return reports; } private boolean prepareGoals( ReportPlugin reportPlugin, PluginDescriptor pluginDescriptor, List<GoalWithConf> goalsWithConfiguration ) { if ( reportPlugin.getReportSets().isEmpty() && reportPlugin.getReports().isEmpty() ) { // by default, use every goal, which will be filtered later to only keep reporting goals List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos(); for ( MojoDescriptor mojoDescriptor : mojoDescriptors ) { goalsWithConfiguration.add( new GoalWithConf( reportPlugin, pluginDescriptor, mojoDescriptor.getGoal(), mojoDescriptor.getConfiguration() ) ); } return false; } Set<String> goals = new HashSet<String>(); for ( String report : reportPlugin.getReports() ) { if ( goals.add( report ) ) { goalsWithConfiguration.add( new GoalWithConf( reportPlugin, pluginDescriptor, report, reportPlugin.getConfiguration() ) ); } else { logger.warn( report + " report is declared twice in default reports" ); } } for ( ReportSet reportSet : reportPlugin.getReportSets() ) { goals = new HashSet<String>(); for ( String report : reportSet.getReports() ) { if ( goals.add( report ) ) { goalsWithConfiguration.add( new GoalWithConf( reportPlugin, pluginDescriptor, report, reportSet.getConfiguration() ) ); } else { logger.warn( report + " report is declared twice in " + reportSet.getId() + " reportSet" ); } } } return true; } private MavenReportExecution prepareReportExecution( MavenReportExecutorRequest mavenReportExecutorRequest, GoalWithConf report, boolean hasUserDefinedReports ) throws Exception { ReportPlugin reportPlugin = report.getReportPlugin(); PluginDescriptor pluginDescriptor = report.getPluginDescriptor(); MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( report.getGoal() ); if ( mojoDescriptor == null ) { throw new MojoNotFoundException( report.getGoal(), pluginDescriptor ); } MavenProject project = mavenReportExecutorRequest.getProject(); if ( !hasUserDefinedReports && mojoDescriptor.isAggregator() && !canAggregate( project ) ) { // aggregator mojos automatically added from plugin are only run at execution root return null; } MojoExecution mojoExecution = new MojoExecution( pluginDescriptor.getPlugin(), report.getGoal(), null ); mojoExecution.setMojoDescriptor( mojoDescriptor ); mavenPluginManagerHelper.setupPluginRealm( pluginDescriptor, mavenReportExecutorRequest.getMavenSession(), Thread.currentThread().getContextClassLoader(), IMPORTS, EXCLUDES ); if ( !isMavenReport( mojoExecution, pluginDescriptor ) ) { if ( hasUserDefinedReports ) { // reports were explicitly written in the POM logger.warn( "ignoring " + mojoExecution.getPlugin().getId() + ':' + report.getGoal() + " goal since it is not a report: should be removed from reporting configuration in POM" ); } return null; } Xpp3Dom pluginMgmtConfiguration = null; if ( project.getBuild() != null && project.getBuild().getPluginManagement() != null ) { Plugin pluginMgmt = find( reportPlugin, project.getBuild().getPluginManagement().getPlugins() ); if ( pluginMgmt != null ) { pluginMgmtConfiguration = (Xpp3Dom) pluginMgmt.getConfiguration(); } } mojoExecution.setConfiguration( mergeConfiguration( mojoDescriptor.getMojoConfiguration(), pluginMgmtConfiguration, reportPlugin.getConfiguration(), report.getConfiguration(), mojoDescriptor.getParameterMap().keySet() ) ); MavenReport mavenReport = getConfiguredMavenReport( mojoExecution, pluginDescriptor, mavenReportExecutorRequest ); MavenReportExecution mavenReportExecution = new MavenReportExecution( report.getGoal(), mojoExecution.getPlugin(), mavenReport, pluginDescriptor.getClassRealm() ); lifecycleExecutor.calculateForkedExecutions( mojoExecution, mavenReportExecutorRequest.getMavenSession() ); if ( !mojoExecution.getForkedExecutions().isEmpty() ) { String reportDescription = pluginDescriptor.getArtifactId() + ":" + report.getGoal() + " report"; String execution; if ( StringUtils.isNotEmpty( mojoDescriptor.getExecutePhase() ) ) { // forked phase execution = "'" + ( StringUtils.isEmpty( mojoDescriptor.getExecuteLifecycle() ) ? "" : ( '[' + mojoDescriptor.getExecuteLifecycle() + ']' ) ) + mojoDescriptor.getExecutePhase() + "' forked phase execution"; } else { // forked goal execution = "'" + mojoDescriptor.getExecuteGoal() + "' forked goal execution"; } logger.info( "preparing " + reportDescription + " requires " + execution ); lifecycleExecutor.executeForkedExecutions( mojoExecution, mavenReportExecutorRequest.getMavenSession() ); logger.info( execution + " for " + reportDescription + " preparation done" ); } return mavenReportExecution; } private boolean canAggregate( MavenProject project ) { return project.isExecutionRoot() && "pom".equals( project.getPackaging() ) && ( project.getModules() != null ) && !project.getModules().isEmpty(); } private MavenReport getConfiguredMavenReport( MojoExecution mojoExecution, PluginDescriptor pluginDescriptor, MavenReportExecutorRequest mavenReportExecutorRequest ) throws PluginContainerException, PluginConfigurationException { try { Mojo mojo = mavenPluginManager.getConfiguredMojo( Mojo.class, mavenReportExecutorRequest.getMavenSession(), mojoExecution ); return (MavenReport) mojo; } catch ( ClassCastException e ) { getLog().warn( "skip ClassCastException " + e.getMessage() ); return null; } catch ( PluginContainerException e ) { /** * ignore old plugin which are using removed PluginRegistry [INFO] Caused by: * java.lang.NoClassDefFoundError: org/apache/maven/plugin/registry/PluginRegistry */ if ( e.getCause() != null && e.getCause() instanceof NoClassDefFoundError && e.getMessage().contains( "PluginRegistry" ) ) { getLog().warn( "skip NoClassDefFoundError with PluginRegistry " ); // too noisy, only in debug mode + e.getMessage() ); if ( getLog().isDebugEnabled() ) { getLog().debug( e.getMessage(), e ); } return null; } throw e; } } private boolean isMavenReport( MojoExecution mojoExecution, PluginDescriptor pluginDescriptor ) { ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); // get the plugin's goal Mojo class Class<?> mojoClass; try { Thread.currentThread().setContextClassLoader( mojoExecution.getMojoDescriptor().getRealm() ); mojoClass = pluginDescriptor.getClassRealm().loadClass( mojoExecution.getMojoDescriptor().getImplementation() ); } catch ( ClassNotFoundException e ) { getLog().warn( "skip ClassNotFoundException mojoExecution.goal '" + mojoExecution.getGoal() + "': " + e.getMessage(), e ); return false; } finally { Thread.currentThread().setContextClassLoader( originalClassLoader ); } // check if it is a report try { Thread.currentThread().setContextClassLoader( mojoExecution.getMojoDescriptor().getRealm() ); MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( mojoExecution.getGoal() ); boolean isMavenReport = MavenReport.class.isAssignableFrom( mojoClass ); if ( getLog().isDebugEnabled() ) { if ( mojoDescriptor != null && mojoDescriptor.getImplementationClass() != null ) { getLog().debug( "class " + mojoDescriptor.getImplementationClass().getName() + " isMavenReport: " + isMavenReport ); } if ( !isMavenReport ) { getLog().debug( "skip non MavenReport " + mojoExecution.getMojoDescriptor().getId() ); } } return isMavenReport; } catch ( LinkageError e ) { getLog().warn( "skip LinkageError mojoExecution.goal '" + mojoExecution.getGoal() + "': " + e.getMessage(), e ); return false; } finally { Thread.currentThread().setContextClassLoader( originalClassLoader ); } } /** * Merge plugin configuration and reportset configuration to mojo configuration to get effective * mojo configuration. * * @param mojoConf configuration done at mojo descriptor level * @param pluginMgmtConfig configuration done at build.pluginManagement level * @param pluginConf configuration done at reporting plugin level * @param reportSetConf configuration done at reportSet level * @param parameters set of supported parameters: any other parameter will be removed * @return the effective configuration to be used */ private Xpp3Dom mergeConfiguration( PlexusConfiguration mojoConf, Xpp3Dom pluginMgmtConfig, PlexusConfiguration pluginConf, PlexusConfiguration reportSetConf, Set<String> parameters ) { Xpp3Dom mojoConfig = ( mojoConf != null ) ? convert( mojoConf ) : new Xpp3Dom( "configuration" ); if ( pluginMgmtConfig != null || pluginConf != null || reportSetConf != null ) { Xpp3Dom pluginConfig = ( pluginConf == null ) ? new Xpp3Dom( "fake" ) : convert( pluginConf ); // merge pluginConf into reportSetConf Xpp3Dom mergedConfig = Xpp3DomUtils.mergeXpp3Dom( convert( reportSetConf ), pluginConfig ); // then merge pluginMgmtConfig mergedConfig = Xpp3DomUtils.mergeXpp3Dom( mergedConfig, pluginMgmtConfig ); // then merge mojoConf mergedConfig = Xpp3DomUtils.mergeXpp3Dom( mergedConfig, mojoConfig ); // clean result Xpp3Dom cleanedConfig = new Xpp3Dom( "configuration" ); if ( mergedConfig.getChildren() != null ) { for ( Xpp3Dom parameter : mergedConfig.getChildren() ) { if ( parameters.contains( parameter.getName() ) ) { cleanedConfig.addChild( parameter ); } } } mojoConfig = cleanedConfig; } return mojoConfig; } private Xpp3Dom convert( PlexusConfiguration config ) { if ( config == null ) { return null; } Xpp3Dom dom = new Xpp3Dom( config.getName() ); dom.setValue( config.getValue( null ) ); for ( String attrib : config.getAttributeNames() ) { dom.setAttribute( attrib, config.getAttribute( attrib, null ) ); } for ( int n = config.getChildCount(), i = 0; i < n; i++ ) { dom.addChild( convert( config.getChild( i ) ) ); } return dom; } private Logger getLog() { return logger; } /** * Resolve report plugin version. Steps to find a plugin version stop after each step if a non <code>null</code> * value has been found: * <ol> * <li>use the one defined in the reportPlugin configuration,</li> * <li>search similar (same groupId and artifactId) mojo in the build/plugins section of the pom,</li> * <li>search similar (same groupId and artifactId) mojo in the build/pluginManagement section of the pom,</li> * <li>ask {@link PluginVersionResolver} to get a fallback version and display a warning as it's not a recommended * use.</li> * </ol> * * @param reportPlugin the report plugin to resolve the version * @param mavenReportExecutorRequest the current report execution context * @return the report plugin version * @throws PluginVersionResolutionException */ protected String resolvePluginVersion( ReportPlugin reportPlugin, MavenReportExecutorRequest mavenReportExecutorRequest ) throws PluginVersionResolutionException { String reportPluginKey = reportPlugin.getGroupId() + ':' + reportPlugin.getArtifactId(); if ( getLog().isDebugEnabled() ) { getLog().debug( "resolving version for " + reportPluginKey ); } // look for version defined in the reportPlugin configuration if ( reportPlugin.getVersion() != null ) { if ( getLog().isDebugEnabled() ) { logger.debug( "resolved " + reportPluginKey + " version from the reporting.plugins section: " + reportPlugin.getVersion() ); } return reportPlugin.getVersion(); } MavenProject project = mavenReportExecutorRequest.getProject(); // search in the build section if ( project.getBuild() != null ) { Plugin plugin = find( reportPlugin, project.getBuild().getPlugins() ); if ( plugin != null && plugin.getVersion() != null ) { if ( getLog().isDebugEnabled() ) { logger.debug( "resolved " + reportPluginKey + " version from the build.plugins section: " + plugin.getVersion() ); } return plugin.getVersion(); } } // search in pluginManagement section if ( project.getBuild() != null && project.getBuild().getPluginManagement() != null ) { Plugin plugin = find( reportPlugin, project.getBuild().getPluginManagement().getPlugins() ); if ( plugin != null && plugin.getVersion() != null ) { if ( getLog().isDebugEnabled() ) { logger.debug( "resolved " + reportPluginKey + " version from the build.pluginManagement.plugins section: " + plugin.getVersion() ); } return plugin.getVersion(); } } logger.warn( "Report plugin " + reportPluginKey + " has an empty version." ); logger.warn( "" ); logger.warn( "It is highly recommended to fix these problems" + " because they threaten the stability of your build." ); logger.warn( "" ); logger.warn( "For this reason, future Maven versions might no" + " longer support building such malformed projects." ); Plugin plugin = new Plugin(); plugin.setGroupId( reportPlugin.getGroupId() ); plugin.setArtifactId( reportPlugin.getArtifactId() ); PluginVersionRequest pluginVersionRequest = new DefaultPluginVersionRequest( plugin, mavenReportExecutorRequest.getMavenSession() ); PluginVersionResult result = pluginVersionResolver.resolve( pluginVersionRequest ); if ( getLog().isDebugEnabled() ) { getLog().debug( "resolved " + reportPluginKey + " version from repository: " + result.getVersion() ); } return result.getVersion(); } /** * Search similar (same groupId and artifactId) plugin as a given report plugin. * * @param reportPlugin the report plugin to search for a similar plugin * @param plugins the candidate plugins * @return the first similar plugin */ private Plugin find( ReportPlugin reportPlugin, List<Plugin> plugins ) { if ( plugins == null ) { return null; } for ( Plugin plugin : plugins ) { if ( StringUtils.equals( plugin.getArtifactId(), reportPlugin.getArtifactId() ) && StringUtils.equals( plugin.getGroupId(), reportPlugin.getGroupId() ) ) { return plugin; } } return null; } /** * TODO other stuff to merge ? * <p> * this method will "merge" some part of the plugin declaration existing in the build section to the fake plugin * build for report execution: * <ul> * <li>dependencies</li> * </ul> * </p> * The plugin could only be present in the dependency management section. * * @param mavenReportExecutorRequest * @param buildPlugin * @param reportPlugin */ private void mergePluginToReportPlugin( MavenReportExecutorRequest mavenReportExecutorRequest, Plugin buildPlugin, ReportPlugin reportPlugin ) { Build build = mavenReportExecutorRequest.getProject().getBuild(); Plugin configuredPlugin = find( reportPlugin, build.getPlugins() ); if ( configuredPlugin == null && build.getPluginManagement() != null ) { configuredPlugin = find( reportPlugin, build.getPluginManagement().getPlugins() ); } if ( configuredPlugin != null ) { if ( !configuredPlugin.getDependencies().isEmpty() ) { buildPlugin.getDependencies().addAll( configuredPlugin.getDependencies() ); } } } private static class GoalWithConf { private final String goal; private final PlexusConfiguration configuration; private final ReportPlugin reportPlugin; private final PluginDescriptor pluginDescriptor; public GoalWithConf( ReportPlugin reportPlugin, PluginDescriptor pluginDescriptor, String goal, PlexusConfiguration configuration ) { this.reportPlugin = reportPlugin; this.pluginDescriptor = pluginDescriptor; this.goal = goal; this.configuration = configuration; } public ReportPlugin getReportPlugin() { return reportPlugin; } public PluginDescriptor getPluginDescriptor() { return pluginDescriptor; } public String getGoal() { return goal; } public PlexusConfiguration getConfiguration() { return configuration; } } }