View Javadoc

1   package org.apache.maven.reporting.exec;
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.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  
30  import org.apache.maven.artifact.repository.DefaultRepositoryRequest;
31  import org.apache.maven.artifact.repository.RepositoryRequest;
32  import org.apache.maven.artifact.resolver.filter.ExclusionSetFilter;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.lifecycle.LifecycleExecutor;
35  import org.apache.maven.model.Plugin;
36  import org.apache.maven.plugin.MavenPluginManager;
37  import org.apache.maven.plugin.Mojo;
38  import org.apache.maven.plugin.MojoExecution;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugin.MojoNotFoundException;
41  import org.apache.maven.plugin.PluginConfigurationException;
42  import org.apache.maven.plugin.PluginContainerException;
43  import org.apache.maven.plugin.descriptor.MojoDescriptor;
44  import org.apache.maven.plugin.descriptor.PluginDescriptor;
45  import org.apache.maven.plugin.version.DefaultPluginVersionRequest;
46  import org.apache.maven.plugin.version.PluginVersionRequest;
47  import org.apache.maven.plugin.version.PluginVersionResolutionException;
48  import org.apache.maven.plugin.version.PluginVersionResolver;
49  import org.apache.maven.plugin.version.PluginVersionResult;
50  import org.apache.maven.project.MavenProject;
51  import org.apache.maven.reporting.MavenReport;
52  import org.codehaus.classworlds.ClassRealm;
53  import org.codehaus.plexus.component.annotations.Component;
54  import org.codehaus.plexus.component.annotations.Requirement;
55  import org.codehaus.plexus.configuration.PlexusConfiguration;
56  import org.codehaus.plexus.logging.Logger;
57  import org.codehaus.plexus.util.StringUtils;
58  import org.codehaus.plexus.util.xml.Xpp3Dom;
59  import org.codehaus.plexus.util.xml.Xpp3DomUtils;
60  import org.sonatype.aether.repository.RemoteRepository;
61  import org.sonatype.aether.util.filter.ExclusionsDependencyFilter;
62  
63  /**
64   * <p>
65   *   This component will build some {@link MavenReportExecution} from {@link MavenReportExecutorRequest}.
66   *   If a {@link MavenReport} need to fork a lifecycle, this fork is executed here. 
67   *   It will ask the core to get some informations in order to correctly setup {@link MavenReport}.
68   * </p>
69   * <p>
70   *   <b>Note</b> if no version is defined in the report plugin the version will be search 
71   *   with method {@link #getPluginVersion(ReportPlugin, RepositoryRequest, MavenReportExecutorRequest)}
72   *   Steps to find a plugin version stop after each step if a non <code>null</code> value has been found:
73   *   <ul>
74   *     <li>use the one defined in the reportPlugin configuration</li>
75   *     <li>search similar (same groupId and artifactId) mojo in the build/plugins section of the pom</li>
76   *     <li>search similar (same groupId and artifactId) mojo in the build/pluginManagement section of the pom</li>
77   *     <li>ask {@link PluginVersionResolver} to get a version and display a warning as it's not a recommended use</li>  
78   *   </ul>
79   * </p>
80   * <p>
81   *   Following steps are done
82   *   <ul>
83   *     <li>get {@link PluginDescriptor} from the {@link MavenPluginManager#getPluginDescriptor(Plugin, RepositoryRequest)}</li>
84   *     <li>setup a {@link ClassLoader} with the Mojo Site plugin {@link ClassLoader} as parent for the report execution. 
85   *       You must note some classes are imported from the current Site Mojo {@link ClassRealm} see {@link #IMPORTS}.
86   *       The artifact resolution excludes the following artifacts (with using an {@link ExclusionSetFilter}: 
87   *       doxia-site-renderer, doxia-sink-api, maven-reporting-api.
88   *       done using {@link MavenPluginManager#setupPluginRealm(PluginDescriptor, org.apache.maven.execution.MavenSession, ClassLoader, List, org.apache.maven.artifact.resolver.filter.ArtifactFilter)}
89   *     </li>
90   *     <li>
91   *       setup the mojo using {@link MavenPluginManager#getConfiguredMojo(Class, org.apache.maven.execution.MavenSession, MojoExecution)}
92   *     </li>
93   *     <li>
94   *       verify with {@link LifecycleExecutor#calculateForkedExecutions(MojoExecution, org.apache.maven.execution.MavenSession)}
95   *       if any forked execution is needed: if yes executes the forked execution here
96   *     </li>
97   *   </ul>
98   * </p>
99   * @author Olivier Lamy
100  * @since 3.0-beta-1
101  */
102 @Component( role = MavenReportExecutor.class )
103 public class DefaultMavenReportExecutor
104     implements MavenReportExecutor
105 {
106     @Requirement
107     private Logger logger;
108 
109     @Requirement
110     protected MavenPluginManager mavenPluginManager;
111 
112     @Requirement
113     protected LifecycleExecutor lifecycleExecutor;
114 
115     @Requirement
116     protected PluginVersionResolver pluginVersionResolver;
117 
118     private static final List<String> IMPORTS = Arrays.asList( "org.apache.maven.reporting.MavenReport",
119                                                                "org.apache.maven.reporting.MavenMultiPageReport",
120                                                                "org.apache.maven.doxia.siterenderer.Renderer",
121                                                                "org.apache.maven.doxia.sink.SinkFactory",
122                                                                "org.codehaus.doxia.sink.Sink",
123                                                                "org.apache.maven.doxia.sink.Sink",
124                                                                "org.apache.maven.doxia.sink.SinkEventAttributes",
125                                                                "org.apache.maven.doxia.logging.LogEnabled",
126                                                                "org.apache.maven.doxia.logging.Log" );
127 
128     private static final ExclusionsDependencyFilter EXCLUDES =
129         new ExclusionsDependencyFilter( Arrays.asList( "doxia-site-renderer", "doxia-sink-api", "maven-reporting-api" ) );
130 
131     public List<MavenReportExecution> buildMavenReports( MavenReportExecutorRequest mavenReportExecutorRequest )
132         throws MojoExecutionException
133     {
134         if ( mavenReportExecutorRequest.getReportPlugins() == null )
135         {
136             return Collections.emptyList();
137         }
138         getLog().debug( "DefaultMavenReportExecutor.buildMavenReports()" );
139 
140         List<String> reportPluginKeys = new ArrayList<String>();
141         List<MavenReportExecution> reports = new ArrayList<MavenReportExecution>();
142 
143         String pluginKey = "";
144         try
145         {
146             for ( ReportPlugin reportPlugin : mavenReportExecutorRequest.getReportPlugins() )
147             {
148                 pluginKey = reportPlugin.getGroupId() + ":" + reportPlugin.getArtifactId();
149 
150                 buildReportPlugin( mavenReportExecutorRequest, reportPlugin, reportPluginKeys, reports );
151             }
152             return reports;
153         }
154         catch ( Exception e )
155         {
156             throw new MojoExecutionException( "failed to get report for " + pluginKey, e );
157         }
158     }
159 
160     protected void buildReportPlugin( MavenReportExecutorRequest mavenReportExecutorRequest, ReportPlugin reportPlugin,
161                                       List<String> reportPluginKeys, List<MavenReportExecution> reports )
162         throws Exception
163     {
164         Plugin plugin = new Plugin();
165         plugin.setGroupId( reportPlugin.getGroupId() );
166         plugin.setArtifactId( reportPlugin.getArtifactId() );
167 
168         String pluginKey = reportPlugin.getGroupId() + ":" + reportPlugin.getArtifactId();
169         if ( reportPluginKeys.contains( pluginKey ) )
170         {
171             logger.info( "plugin " + pluginKey + " will be executed more than one time" );
172         }
173         else
174         {
175             reportPluginKeys.add( pluginKey );
176         }
177 
178         RepositoryRequest repositoryRequest = new DefaultRepositoryRequest();
179         repositoryRequest.setLocalRepository( mavenReportExecutorRequest.getLocalRepository() );
180         repositoryRequest.setRemoteRepositories( mavenReportExecutorRequest.getProject().getPluginArtifactRepositories() );
181 
182         plugin.setVersion( getPluginVersion( reportPlugin, repositoryRequest, mavenReportExecutorRequest ) );
183 
184         mergePluginToReportPlugin( mavenReportExecutorRequest, plugin, reportPlugin );
185 
186         logger.info( "configuring report plugin " + plugin.getId() );
187 
188         MavenSession session = mavenReportExecutorRequest.getMavenSession();
189         List<RemoteRepository> remoteRepositories = session.getCurrentProject().getRemotePluginRepositories();
190 
191         PluginDescriptor pluginDescriptor =
192             mavenPluginManager.getPluginDescriptor( plugin, remoteRepositories, session.getRepositorySession() );
193 
194         Map<String, PlexusConfiguration> goalsWithConfiguration = new LinkedHashMap<String, PlexusConfiguration>();
195 
196         if ( reportPlugin.getReportSets().isEmpty() && reportPlugin.getReports().isEmpty() )
197         {
198             List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
199             for ( MojoDescriptor mojoDescriptor : mojoDescriptors )
200             {
201                 goalsWithConfiguration.put( mojoDescriptor.getGoal(), mojoDescriptor.getConfiguration() );
202             }
203         }
204         else
205         {
206             for ( ReportSet reportSet : reportPlugin.getReportSets() )
207             {
208                 for ( String report : reportSet.getReports() )
209                 {
210                     goalsWithConfiguration.put( report, reportSet.getConfiguration() );
211                 }
212             }
213 
214             for ( String report : reportPlugin.getReports() )
215             {
216                 goalsWithConfiguration.put( report, reportPlugin.getConfiguration() );
217             }
218         }
219 
220         for ( Entry<String, PlexusConfiguration> entry : goalsWithConfiguration.entrySet() )
221         {
222             MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( entry.getKey() );
223             if ( mojoDescriptor == null )
224             {
225                 throw new MojoNotFoundException( entry.getKey(), pluginDescriptor );
226             }
227 
228             MojoExecution mojoExecution = new MojoExecution( plugin, entry.getKey(), "report:" + entry.getKey() );
229 
230             mojoExecution.setConfiguration( convert( mojoDescriptor ) );
231 
232             if ( reportPlugin.getConfiguration() != null || entry.getValue() != null )
233             {
234                 Xpp3Dom reportConfiguration =
235                     reportPlugin.getConfiguration() == null ? new Xpp3Dom( "fake" )
236                                     : convert( reportPlugin.getConfiguration() );
237 
238                 // MSITE-512 configuration from ReportSet must win
239                 Xpp3Dom mergedConfigurationWithReportSet =
240                     Xpp3DomUtils.mergeXpp3Dom( convert( entry.getValue() ), reportConfiguration );
241 
242                 Xpp3Dom mergedConfiguration =
243                     Xpp3DomUtils.mergeXpp3Dom( mergedConfigurationWithReportSet, convert( mojoDescriptor ) );
244 
245                 Xpp3Dom cleanedConfiguration = new Xpp3Dom( "configuration" );
246                 if ( mergedConfiguration.getChildren() != null )
247                 {
248                     for ( Xpp3Dom parameter : mergedConfiguration.getChildren() )
249                     {
250                         if ( mojoDescriptor.getParameterMap().containsKey( parameter.getName() ) )
251                         {
252                             cleanedConfiguration.addChild( parameter );
253                         }
254                     }
255                 }
256                 if ( getLog().isDebugEnabled() )
257                 {
258                     getLog().debug( "mojoExecution mergedConfiguration: " + mergedConfiguration );
259                     getLog().debug( "mojoExecution cleanedConfiguration: " + cleanedConfiguration );
260                 }
261 
262                 mojoExecution.setConfiguration( cleanedConfiguration );
263             }
264 
265             mojoExecution.setMojoDescriptor( mojoDescriptor );
266 
267             mavenPluginManager.setupPluginRealm( pluginDescriptor, mavenReportExecutorRequest.getMavenSession(),
268                                                  Thread.currentThread().getContextClassLoader(), IMPORTS, EXCLUDES );
269             MavenReport mavenReport =
270                 getConfiguredMavenReport( mojoExecution, pluginDescriptor, mavenReportExecutorRequest );
271 
272             if ( mavenReport == null )
273             {
274                 continue;
275             }
276 
277             MavenReportExecution mavenReportExecution =
278                 new MavenReportExecution( mojoExecution.getPlugin(), mavenReport, pluginDescriptor.getClassRealm() );
279 
280             lifecycleExecutor.calculateForkedExecutions( mojoExecution, mavenReportExecutorRequest.getMavenSession() );
281 
282             if ( !mojoExecution.getForkedExecutions().isEmpty() )
283             {
284                 lifecycleExecutor.executeForkedExecutions( mojoExecution, mavenReportExecutorRequest.getMavenSession() );
285             }
286 
287             if ( canGenerateReport( mavenReport, mojoExecution ) )
288             {
289                 reports.add( mavenReportExecution );
290             }
291         }
292     }
293 
294     private boolean canGenerateReport( MavenReport mavenReport, MojoExecution mojoExecution )
295     {
296         ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
297         try
298         {
299             Thread.currentThread().setContextClassLoader( mojoExecution.getMojoDescriptor().getRealm() );
300 
301             return mavenReport.canGenerateReport();
302         }
303         finally
304         {
305             Thread.currentThread().setContextClassLoader( originalClassLoader );
306         } 
307     }
308 
309     private MavenReport getConfiguredMavenReport( MojoExecution mojoExecution, PluginDescriptor pluginDescriptor,
310                                                   MavenReportExecutorRequest mavenReportExecutorRequest )
311         throws PluginContainerException, PluginConfigurationException
312     {
313         try
314         {
315             if ( !isMavenReport( mojoExecution, pluginDescriptor ) )
316             {
317                 return null;
318             }
319 
320             Mojo mojo = mavenPluginManager.getConfiguredMojo( Mojo.class,
321                                                               mavenReportExecutorRequest.getMavenSession(),
322                                                               mojoExecution );
323 
324             return (MavenReport) mojo;
325         }
326         catch ( ClassCastException e )
327         {
328             getLog().warn( "skip ClassCastException " + e.getMessage() );
329             return null;
330         }
331         catch ( PluginContainerException e )
332         {
333             /**
334              * ignore old plugin which are using removed PluginRegistry
335              * [INFO] Caused by: java.lang.NoClassDefFoundError: org/apache/maven/plugin/registry/PluginRegistry
336              */
337             if ( e.getCause() != null && e.getCause() instanceof NoClassDefFoundError
338                 && e.getMessage().contains( "PluginRegistry" ) )
339             {
340                 getLog().warn( "skip NoClassDefFoundError with PluginRegistry " );
341                 // too noisy, only in debug mode + e.getMessage() );
342                 if ( getLog().isDebugEnabled() )
343                 {
344                     getLog().debug( e.getMessage(), e );
345                 }
346                 return null;
347             }
348             throw e;
349         }
350     }
351 
352     private boolean isMavenReport( MojoExecution mojoExecution, PluginDescriptor pluginDescriptor )
353     {
354         ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
355         Class<?> mojoClass;
356         Thread.currentThread().setContextClassLoader( mojoExecution.getMojoDescriptor().getRealm() );
357         try
358         {
359             mojoClass =
360                 pluginDescriptor.getClassRealm().loadClass( mojoExecution.getMojoDescriptor().getImplementation() );
361         }
362         catch ( ClassNotFoundException e )
363         {
364             getLog().warn( "skip ClassNotFoundException mojoExecution.goal '" + mojoExecution.getGoal() + "': "
365                                + e.getMessage(), e );
366             return false;
367         }
368         finally
369         {
370             Thread.currentThread().setContextClassLoader( originalClassLoader );
371         }       
372 
373         try
374         {
375             Thread.currentThread().setContextClassLoader( mojoExecution.getMojoDescriptor().getRealm() );
376             MojoDescriptor mojoDescriptor = pluginDescriptor.getMojo( mojoExecution.getGoal() );
377 
378             boolean isMavenReport = MavenReport.class.isAssignableFrom( mojoClass );
379 
380             if ( getLog().isDebugEnabled() )
381             {
382                 if ( mojoDescriptor != null && mojoDescriptor.getImplementationClass() != null )
383                 {
384                     getLog().debug( "class " + mojoDescriptor.getImplementationClass().getName() + " isMavenReport: "
385                                         + isMavenReport );
386                 }
387 
388                 if ( !isMavenReport )
389                 {
390                     getLog().debug( "skip non MavenReport " + mojoExecution.getMojoDescriptor().getId() );
391                 }
392             }
393 
394             return isMavenReport;
395         }
396         catch ( LinkageError e )
397         {
398             getLog().warn( "skip LinkageError mojoExecution.goal '" + mojoExecution.getGoal() + "': " + e.getMessage(),
399                            e );
400             return false;
401         }
402         finally
403         {
404             Thread.currentThread().setContextClassLoader( originalClassLoader );
405         }
406     }
407 
408     private Xpp3Dom convert( MojoDescriptor mojoDescriptor )
409     {
410         PlexusConfiguration config = mojoDescriptor.getMojoConfiguration();
411         return ( config != null ) ? convert( config ) : new Xpp3Dom( "configuration" );
412     }
413 
414     private Xpp3Dom convert( PlexusConfiguration config )
415     {
416         if ( config == null )
417         {
418             return null;
419         }
420 
421         Xpp3Dom dom = new Xpp3Dom( config.getName() );
422         dom.setValue( config.getValue( null ) );
423 
424         for ( String attrib : config.getAttributeNames() )
425         {
426             dom.setAttribute( attrib, config.getAttribute( attrib, null ) );
427         }
428 
429         for ( int n = config.getChildCount(), i = 0; i < n; i++ )
430         {
431             dom.addChild( convert( config.getChild( i ) ) );
432         }
433 
434         return dom;
435     }
436 
437     private Logger getLog()
438     {
439         return logger;
440     }
441 
442     /**
443      * Resolve report plugin version. 
444      * Steps to find a plugin version stop after each step if a non <code>null</code> value has been found:
445      * <ol>
446      *   <li>use the one defined in the reportPlugin configuration</li>
447      *   <li>search similar (same groupId and artifactId) mojo in the build/plugins section of the pom</li>
448      *   <li>search similar (same groupId and artifactId) mojo in the build/pluginManagement section of the pom</li>
449      *   <li>ask {@link PluginVersionResolver} to get a version and display a warning as it's not a recommended use</li>  
450      * </ol>
451      *
452      * @param reportPlugin the report plugin to resolve the version
453      * @param repositoryRequest TODO: unused, to be removed?
454      * @param mavenReportExecutorRequest the current report execution context
455      * @return the report plugin version
456      * @throws PluginVersionResolutionException
457      */
458     protected String getPluginVersion( ReportPlugin reportPlugin, RepositoryRequest repositoryRequest,
459                                        MavenReportExecutorRequest mavenReportExecutorRequest )
460         throws PluginVersionResolutionException
461     {
462         String reportPluginKey = reportPlugin.getGroupId() + ':' + reportPlugin.getArtifactId();
463         if ( getLog().isDebugEnabled() )
464         {
465             getLog().debug( "resolving version for " + reportPluginKey );
466         }
467 
468         // look for version defined in the reportPlugin configuration
469         if ( reportPlugin.getVersion() != null )
470         {
471             if ( getLog().isDebugEnabled() )
472             {
473                 logger.debug( "resolved " + reportPluginKey + " version from the reporting.plugins section: "
474                     + reportPlugin.getVersion() );
475             }
476             return reportPlugin.getVersion();
477         }
478 
479         MavenProject project = mavenReportExecutorRequest.getProject();
480 
481         // search in the build section
482         if ( project.getBuild() != null )
483         {
484             Plugin plugin = find( reportPlugin, project.getBuild().getPlugins() );
485 
486             if ( plugin != null && plugin.getVersion() != null )
487             {
488                 if ( getLog().isDebugEnabled() )
489                 {
490                     logger.debug( "resolved " + reportPluginKey + " version from the build.plugins section: "
491                         + plugin.getVersion() );
492                 }
493                 return plugin.getVersion();
494             }
495         }
496 
497         // search in pluginManagement section
498         if ( project.getBuild() != null && project.getBuild().getPluginManagement() != null )
499         {
500             Plugin plugin = find( reportPlugin, project.getBuild().getPluginManagement().getPlugins() );
501 
502             if ( plugin != null && plugin.getVersion() != null )
503             {
504                 if ( getLog().isDebugEnabled() )
505                 {
506                     logger.debug( "resolved " + reportPluginKey
507                         + " version from the build.pluginManagement.plugins section: " + plugin.getVersion() );
508                 }
509                 return plugin.getVersion();
510             }
511         }
512 
513 
514         logger.warn( "Report plugin " + reportPluginKey + " has an empty version." );
515         logger.warn( "" );
516         logger.warn( "It is highly recommended to fix these problems"
517             + " because they threaten the stability of your build." );
518         logger.warn( "" );
519         logger.warn( "For this reason, future Maven versions might no"
520             + " longer support building such malformed projects." );
521 
522         Plugin plugin = new Plugin();
523         plugin.setGroupId( reportPlugin.getGroupId() );
524         plugin.setArtifactId( reportPlugin.getArtifactId() );
525         
526         PluginVersionRequest pluginVersionRequest =
527             new DefaultPluginVersionRequest( plugin, mavenReportExecutorRequest.getMavenSession() );
528 
529         PluginVersionResult result = pluginVersionResolver.resolve( pluginVersionRequest );
530         if ( getLog().isDebugEnabled() )
531         {
532             getLog().debug( "resolved " + reportPluginKey + " version from repository: " + result.getVersion() );
533         }
534         return result.getVersion();
535     }
536 
537     /**
538      * Search similar (same groupId and artifactId) mojo as a given report plugin.
539      * 
540      * @param reportPlugin the report plugin to search for a similar mojo
541      * @param plugins the candidate mojos 
542      * @return the first similar mojo
543      */
544     private Plugin find( ReportPlugin reportPlugin, List<Plugin> plugins )
545     {
546         if ( plugins == null )
547         {
548             return null;
549         }
550         for ( Plugin plugin : plugins )
551         {
552             if ( StringUtils.equals( plugin.getArtifactId(), reportPlugin.getArtifactId() )
553                 && StringUtils.equals( plugin.getGroupId(), reportPlugin.getGroupId() ) )
554             {
555                 return plugin;
556             }
557         }
558         return null;
559     }
560 
561     /**
562      * TODO other stuff to merge ?
563      * <p>
564      * this method will "merge" some part of the plugin declaration existing in the build section 
565      * to the fake plugin build for report execution:
566      * <ul>
567      *   <li>dependencies</li>
568      * </ul>
569      * </p>
570      * @param mavenReportExecutorRequest
571      * @param buildPlugin
572      * @param reportPlugin
573      */
574     private void mergePluginToReportPlugin( MavenReportExecutorRequest mavenReportExecutorRequest, Plugin buildPlugin,
575                                             ReportPlugin reportPlugin )
576     {
577         Plugin configuredPlugin = find( reportPlugin, mavenReportExecutorRequest.getProject().getBuild().getPlugins() );
578         if ( configuredPlugin != null )
579         {
580             if ( !configuredPlugin.getDependencies().isEmpty() )
581             {
582                 buildPlugin.getDependencies().addAll( configuredPlugin.getDependencies() );
583             }
584         }
585     }   
586 }