View Javadoc
1   package org.apache.maven.lifecycle.internal;
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 org.apache.maven.artifact.Artifact;
23  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
24  import org.apache.maven.artifact.resolver.filter.CumulativeScopeArtifactFilter;
25  import org.apache.maven.execution.ExecutionEvent;
26  import org.apache.maven.execution.MavenSession;
27  import org.apache.maven.internal.MultilineMessageHelper;
28  import org.apache.maven.lifecycle.LifecycleExecutionException;
29  import org.apache.maven.lifecycle.MissingProjectException;
30  import org.apache.maven.plugin.BuildPluginManager;
31  import org.apache.maven.plugin.MavenPluginManager;
32  import org.apache.maven.plugin.MojoExecution;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugin.MojoFailureException;
35  import org.apache.maven.plugin.PluginConfigurationException;
36  import org.apache.maven.plugin.PluginIncompatibleException;
37  import org.apache.maven.plugin.PluginManagerException;
38  import org.apache.maven.plugin.descriptor.MojoDescriptor;
39  import org.apache.maven.project.MavenProject;
40  import org.codehaus.plexus.component.annotations.Component;
41  import org.codehaus.plexus.component.annotations.Requirement;
42  import org.codehaus.plexus.util.StringUtils;
43  import org.eclipse.aether.SessionData;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.Collection;
50  import java.util.Collections;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Set;
54  import java.util.TreeSet;
55  import java.util.concurrent.ConcurrentHashMap;
56  import java.util.concurrent.ConcurrentMap;
57  import java.util.concurrent.locks.Lock;
58  import java.util.concurrent.locks.ReentrantLock;
59  import java.util.concurrent.locks.ReentrantReadWriteLock;
60  
61  /**
62   * <p>
63   * Executes an individual mojo
64   * </p>
65   * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
66   *
67   * @author Jason van Zyl
68   * @author Benjamin Bentmann
69   * @author Kristian Rosenvold
70   * @since 3.0
71   */
72  @Component( role = MojoExecutor.class )
73  public class MojoExecutor
74  {
75  
76      private static final Logger LOGGER = LoggerFactory.getLogger( MojoExecutor.class );
77  
78      @Requirement
79      private BuildPluginManager pluginManager;
80  
81      @Requirement
82      private MavenPluginManager mavenPluginManager;
83  
84      @Requirement
85      private LifecycleDependencyResolver lifeCycleDependencyResolver;
86  
87      @Requirement
88      private ExecutionEventCatapult eventCatapult;
89  
90      private final OwnerReentrantReadWriteLock aggregatorLock = new OwnerReentrantReadWriteLock();
91  
92      private final Map<Thread, MojoDescriptor> mojos = new ConcurrentHashMap<>();
93  
94      public MojoExecutor()
95      {
96      }
97  
98      public DependencyContext newDependencyContext( MavenSession session, List<MojoExecution> mojoExecutions )
99      {
100         Set<String> scopesToCollect = new TreeSet<>();
101         Set<String> scopesToResolve = new TreeSet<>();
102 
103         collectDependencyRequirements( scopesToResolve, scopesToCollect, mojoExecutions );
104 
105         return new DependencyContext( session.getCurrentProject(), scopesToCollect, scopesToResolve );
106     }
107 
108     private void collectDependencyRequirements( Set<String> scopesToResolve, Set<String> scopesToCollect,
109                                                 Collection<MojoExecution> mojoExecutions )
110     {
111         for ( MojoExecution mojoExecution : mojoExecutions )
112         {
113             MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
114 
115             scopesToResolve.addAll( toScopes( mojoDescriptor.getDependencyResolutionRequired() ) );
116 
117             scopesToCollect.addAll( toScopes( mojoDescriptor.getDependencyCollectionRequired() ) );
118         }
119     }
120 
121     private Collection<String> toScopes( String classpath )
122     {
123         Collection<String> scopes = Collections.emptyList();
124 
125         if ( StringUtils.isNotEmpty( classpath ) )
126         {
127             if ( Artifact.SCOPE_COMPILE.equals( classpath ) )
128             {
129                 scopes = Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED );
130             }
131             else if ( Artifact.SCOPE_RUNTIME.equals( classpath ) )
132             {
133                 scopes = Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME );
134             }
135             else if ( Artifact.SCOPE_COMPILE_PLUS_RUNTIME.equals( classpath ) )
136             {
137                 scopes = Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED,
138                                         Artifact.SCOPE_RUNTIME );
139             }
140             else if ( Artifact.SCOPE_RUNTIME_PLUS_SYSTEM.equals( classpath ) )
141             {
142                 scopes = Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_RUNTIME );
143             }
144             else if ( Artifact.SCOPE_TEST.equals( classpath ) )
145             {
146                 scopes = Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_SYSTEM, Artifact.SCOPE_PROVIDED,
147                                         Artifact.SCOPE_RUNTIME, Artifact.SCOPE_TEST );
148             }
149         }
150         return Collections.unmodifiableCollection( scopes );
151     }
152 
153     public void execute( MavenSession session, List<MojoExecution> mojoExecutions, ProjectIndex projectIndex )
154         throws LifecycleExecutionException
155 
156     {
157         DependencyContext dependencyContext = newDependencyContext( session, mojoExecutions );
158 
159         PhaseRecorder phaseRecorder = new PhaseRecorder( session.getCurrentProject() );
160 
161         for ( MojoExecution mojoExecution : mojoExecutions )
162         {
163             execute( session, mojoExecution, projectIndex, dependencyContext, phaseRecorder );
164         }
165     }
166 
167     public void execute( MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex,
168                          DependencyContext dependencyContext, PhaseRecorder phaseRecorder )
169         throws LifecycleExecutionException
170     {
171         execute( session, mojoExecution, projectIndex, dependencyContext );
172         phaseRecorder.observeExecution( mojoExecution );
173     }
174 
175     private void execute( MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex,
176                           DependencyContext dependencyContext )
177         throws LifecycleExecutionException
178     {
179         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
180 
181         try
182         {
183             mavenPluginManager.checkRequiredMavenVersion( mojoDescriptor.getPluginDescriptor() );
184         }
185         catch ( PluginIncompatibleException e )
186         {
187             throw new LifecycleExecutionException( mojoExecution, session.getCurrentProject(), e );
188         }
189 
190         if ( mojoDescriptor.isProjectRequired() && !session.getRequest().isProjectPresent() )
191         {
192             Throwable cause = new MissingProjectException(
193                 "Goal requires a project to execute" + " but there is no POM in this directory ("
194                     + session.getExecutionRootDirectory() + ")."
195                     + " Please verify you invoked Maven from the correct directory." );
196             throw new LifecycleExecutionException( mojoExecution, null, cause );
197         }
198 
199         if ( mojoDescriptor.isOnlineRequired() && session.isOffline() )
200         {
201             if ( MojoExecution.Source.CLI.equals( mojoExecution.getSource() ) )
202             {
203                 Throwable cause = new IllegalStateException(
204                     "Goal requires online mode for execution" + " but Maven is currently offline." );
205                 throw new LifecycleExecutionException( mojoExecution, session.getCurrentProject(), cause );
206             }
207             else
208             {
209                 eventCatapult.fire( ExecutionEvent.Type.MojoSkipped, session, mojoExecution );
210 
211                 return;
212             }
213         }
214 
215         doExecute( session, mojoExecution, projectIndex, dependencyContext );
216     }
217 
218     /**
219      * Aggregating mojo executions (possibly) modify all MavenProjects, including those that are currently in use
220      * by concurrently running mojo executions. To prevent race conditions, an aggregating execution will block
221      * all other executions until finished.
222      * We also lock on a given project to forbid a forked lifecycle to be executed concurrently with the project.
223      * TODO: ideally, the builder should take care of the ordering in a smarter way
224      * TODO: and concurrency issues fixed with MNG-7157
225      */
226     private class ProjectLock implements AutoCloseable
227     {
228         final Lock acquiredAggregatorLock;
229         final OwnerReentrantLock acquiredProjectLock;
230 
231         ProjectLock( MavenSession session, MojoDescriptor mojoDescriptor )
232         {
233             mojos.put( Thread.currentThread(), mojoDescriptor );
234             if ( session.getRequest().getDegreeOfConcurrency() > 1 )
235             {
236                 boolean aggregator = mojoDescriptor.isAggregator();
237                 acquiredAggregatorLock = aggregator ? aggregatorLock.writeLock() : aggregatorLock.readLock();
238                 acquiredProjectLock = getProjectLock( session );
239                 if ( !acquiredAggregatorLock.tryLock() )
240                 {
241                     Thread owner = aggregatorLock.getOwner();
242                     MojoDescriptor ownerMojo = owner != null ? mojos.get( owner ) : null;
243                     String str = ownerMojo != null ? " The " + ownerMojo.getId() : "An";
244                     String msg = str + " aggregator mojo is already being executed "
245                             + "in this parallel build, those kind of mojos require exclusive access to "
246                             + "reactor to prevent race conditions. This mojo execution will be blocked "
247                             + "until the aggregator mojo is done.";
248                     warn( msg );
249                     acquiredAggregatorLock.lock();
250                 }
251                 if ( !acquiredProjectLock.tryLock() )
252                 {
253                     Thread owner = acquiredProjectLock.getOwner();
254                     MojoDescriptor ownerMojo = owner != null ? mojos.get( owner ) : null;
255                     String str = ownerMojo != null ? " The " + ownerMojo.getId() : "A";
256                     String msg = str + " mojo is already being executed "
257                             + "on the project " + session.getCurrentProject().getGroupId()
258                             + ":" + session.getCurrentProject().getArtifactId() + ". "
259                             + "This mojo execution will be blocked "
260                             + "until the mojo is done.";
261                     warn( msg );
262                     acquiredProjectLock.lock();
263                 }
264             }
265             else
266             {
267                 acquiredAggregatorLock = null;
268                 acquiredProjectLock = null;
269             }
270         }
271 
272         @Override
273         public void close()
274         {
275             // release the lock in the reverse order of the acquisition
276             if ( acquiredProjectLock != null )
277             {
278                 acquiredProjectLock.unlock();
279             }
280             if ( acquiredAggregatorLock != null )
281             {
282                 acquiredAggregatorLock.unlock();
283             }
284             mojos.remove( Thread.currentThread() );
285         }
286 
287         @SuppressWarnings( { "unchecked", "rawtypes" } )
288         private OwnerReentrantLock getProjectLock( MavenSession session )
289         {
290             SessionData data = session.getRepositorySession().getData();
291             ConcurrentMap<MavenProject, OwnerReentrantLock> locks = ( ConcurrentMap ) data.get( ProjectLock.class );
292             // initialize the value if not already done (in case of a concurrent access) to the method
293             if ( locks == null )
294             {
295                 // the call to data.set(k, null, v) is effectively a call to data.putIfAbsent(k, v)
296                 data.set( ProjectLock.class, null, new ConcurrentHashMap<>() );
297                 locks = ( ConcurrentMap ) data.get( ProjectLock.class );
298             }
299             OwnerReentrantLock acquiredProjectLock = locks.get( session.getCurrentProject() );
300             if ( acquiredProjectLock == null )
301             {
302                 acquiredProjectLock = new OwnerReentrantLock();
303                 OwnerReentrantLock prev = locks.putIfAbsent( session.getCurrentProject(), acquiredProjectLock );
304                 if ( prev != null )
305                 {
306                     acquiredProjectLock = prev;
307                 }
308             }
309             return acquiredProjectLock;
310         }
311     }
312 
313     static class OwnerReentrantLock extends ReentrantLock
314     {
315         @Override
316         public Thread getOwner()
317         {
318             return super.getOwner();
319         }
320     }
321 
322     static class OwnerReentrantReadWriteLock extends ReentrantReadWriteLock
323     {
324         @Override
325         public Thread getOwner()
326         {
327             return super.getOwner();
328         }
329     }
330 
331     private static void warn( String msg )
332     {
333         for ( String s : MultilineMessageHelper.format( msg ) )
334         {
335             LOGGER.warn( s );
336         }
337     }
338 
339     private void doExecute( MavenSession session, MojoExecution mojoExecution, ProjectIndex projectIndex,
340                             DependencyContext dependencyContext )
341             throws LifecycleExecutionException
342     {
343         MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor();
344 
345         List<MavenProject> forkedProjects = executeForkedExecutions( mojoExecution, session, projectIndex );
346 
347         ensureDependenciesAreResolved( mojoDescriptor, session, dependencyContext );
348 
349         try ( ProjectLock lock = new ProjectLock( session, mojoDescriptor ) )
350         {
351             doExecute2( session, mojoExecution );
352         }
353         finally
354         {
355             for ( MavenProject forkedProject : forkedProjects )
356             {
357                 forkedProject.setExecutionProject( null );
358             }
359         }
360     }
361 
362     private void doExecute2( MavenSession session, MojoExecution mojoExecution )
363             throws LifecycleExecutionException
364     {
365         eventCatapult.fire( ExecutionEvent.Type.MojoStarted, session, mojoExecution );
366         try
367         {
368             try
369             {
370                 pluginManager.executeMojo( session, mojoExecution );
371             }
372             catch ( MojoFailureException | PluginManagerException | PluginConfigurationException
373                 | MojoExecutionException e )
374             {
375                 throw new LifecycleExecutionException( mojoExecution, session.getCurrentProject(), e );
376             }
377 
378             eventCatapult.fire( ExecutionEvent.Type.MojoSucceeded, session, mojoExecution );
379         }
380         catch ( LifecycleExecutionException e )
381         {
382             eventCatapult.fire( ExecutionEvent.Type.MojoFailed, session, mojoExecution, e );
383 
384             throw e;
385         }
386     }
387 
388     public void ensureDependenciesAreResolved( MojoDescriptor mojoDescriptor, MavenSession session,
389                                                DependencyContext dependencyContext )
390         throws LifecycleExecutionException
391 
392     {
393         MavenProject project = dependencyContext.getProject();
394         boolean aggregating = mojoDescriptor.isAggregator();
395 
396         if ( dependencyContext.isResolutionRequiredForCurrentProject() )
397         {
398             Collection<String> scopesToCollect = dependencyContext.getScopesToCollectForCurrentProject();
399             Collection<String> scopesToResolve = dependencyContext.getScopesToResolveForCurrentProject();
400 
401             lifeCycleDependencyResolver.resolveProjectDependencies( project, scopesToCollect, scopesToResolve, session,
402                                                                     aggregating, Collections.<Artifact>emptySet() );
403 
404             dependencyContext.synchronizeWithProjectState();
405         }
406 
407         if ( aggregating )
408         {
409             Collection<String> scopesToCollect = toScopes( mojoDescriptor.getDependencyCollectionRequired() );
410             Collection<String> scopesToResolve = toScopes( mojoDescriptor.getDependencyResolutionRequired() );
411 
412             if ( dependencyContext.isResolutionRequiredForAggregatedProjects( scopesToCollect, scopesToResolve ) )
413             {
414                 for ( MavenProject aggregatedProject : session.getProjects() )
415                 {
416                     if ( aggregatedProject != project )
417                     {
418                         lifeCycleDependencyResolver.resolveProjectDependencies( aggregatedProject, scopesToCollect,
419                                                                                 scopesToResolve, session, aggregating,
420                                                                                 Collections.<Artifact>emptySet() );
421                     }
422                 }
423             }
424         }
425 
426         ArtifactFilter artifactFilter = getArtifactFilter( mojoDescriptor );
427         List<MavenProject> projectsToResolve =
428             LifecycleDependencyResolver.getProjects( session.getCurrentProject(), session,
429                                                      mojoDescriptor.isAggregator() );
430         for ( MavenProject projectToResolve : projectsToResolve )
431         {
432             projectToResolve.setArtifactFilter( artifactFilter );
433         }
434     }
435 
436     private ArtifactFilter getArtifactFilter( MojoDescriptor mojoDescriptor )
437     {
438         String scopeToResolve = mojoDescriptor.getDependencyResolutionRequired();
439         String scopeToCollect = mojoDescriptor.getDependencyCollectionRequired();
440 
441         List<String> scopes = new ArrayList<>( 2 );
442         if ( StringUtils.isNotEmpty( scopeToCollect ) )
443         {
444             scopes.add( scopeToCollect );
445         }
446         if ( StringUtils.isNotEmpty( scopeToResolve ) )
447         {
448             scopes.add( scopeToResolve );
449         }
450 
451         if ( scopes.isEmpty() )
452         {
453             return null;
454         }
455         else
456         {
457             return new CumulativeScopeArtifactFilter( scopes );
458         }
459     }
460 
461     public List<MavenProject> executeForkedExecutions( MojoExecution mojoExecution, MavenSession session,
462                                                        ProjectIndex projectIndex )
463         throws LifecycleExecutionException
464     {
465         List<MavenProject> forkedProjects = Collections.emptyList();
466 
467         Map<String, List<MojoExecution>> forkedExecutions = mojoExecution.getForkedExecutions();
468 
469         if ( !forkedExecutions.isEmpty() )
470         {
471             eventCatapult.fire( ExecutionEvent.Type.ForkStarted, session, mojoExecution );
472 
473             MavenProject project = session.getCurrentProject();
474 
475             forkedProjects = new ArrayList<>( forkedExecutions.size() );
476 
477             try
478             {
479                 for ( Map.Entry<String, List<MojoExecution>> fork : forkedExecutions.entrySet() )
480                 {
481                     String projectId = fork.getKey();
482 
483                     int index = projectIndex.getIndices().get( projectId );
484 
485                     MavenProject forkedProject = projectIndex.getProjects().get( projectId );
486 
487                     forkedProjects.add( forkedProject );
488 
489                     MavenProject executedProject = forkedProject.clone();
490 
491                     forkedProject.setExecutionProject( executedProject );
492 
493                     List<MojoExecution> mojoExecutions = fork.getValue();
494 
495                     if ( mojoExecutions.isEmpty() )
496                     {
497                         continue;
498                     }
499 
500                     try
501                     {
502                         session.setCurrentProject( executedProject );
503                         session.getProjects().set( index, executedProject );
504                         projectIndex.getProjects().put( projectId, executedProject );
505 
506                         eventCatapult.fire( ExecutionEvent.Type.ForkedProjectStarted, session, mojoExecution );
507 
508                         execute( session, mojoExecutions, projectIndex );
509 
510                         eventCatapult.fire( ExecutionEvent.Type.ForkedProjectSucceeded, session, mojoExecution );
511                     }
512                     catch ( LifecycleExecutionException e )
513                     {
514                         eventCatapult.fire( ExecutionEvent.Type.ForkedProjectFailed, session, mojoExecution, e );
515 
516                         throw e;
517                     }
518                     finally
519                     {
520                         projectIndex.getProjects().put( projectId, forkedProject );
521                         session.getProjects().set( index, forkedProject );
522                         session.setCurrentProject( project );
523                     }
524                 }
525 
526                 eventCatapult.fire( ExecutionEvent.Type.ForkSucceeded, session, mojoExecution );
527             }
528             catch ( LifecycleExecutionException e )
529             {
530                 eventCatapult.fire( ExecutionEvent.Type.ForkFailed, session, mojoExecution, e );
531 
532                 throw e;
533             }
534         }
535 
536         return forkedProjects;
537     }
538 }