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