View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.lifecycle.internal;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  
24  import java.io.File;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.LinkedHashSet;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.stream.Collectors;
34  
35  import org.apache.maven.RepositoryUtils;
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.artifact.ArtifactUtils;
38  import org.apache.maven.eventspy.internal.EventSpyDispatcher;
39  import org.apache.maven.execution.MavenSession;
40  import org.apache.maven.lifecycle.LifecycleExecutionException;
41  import org.apache.maven.message.MessageBuilderFactory;
42  import org.apache.maven.project.DefaultDependencyResolutionRequest;
43  import org.apache.maven.project.DependencyResolutionException;
44  import org.apache.maven.project.DependencyResolutionResult;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.maven.project.ProjectDependenciesResolver;
47  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
48  import org.apache.maven.project.artifact.ProjectArtifactsCache;
49  import org.codehaus.plexus.logging.Logger;
50  import org.eclipse.aether.graph.Dependency;
51  import org.eclipse.aether.graph.DependencyFilter;
52  import org.eclipse.aether.graph.DependencyNode;
53  import org.eclipse.aether.util.filter.AndDependencyFilter;
54  import org.eclipse.aether.util.filter.ScopeDependencyFilter;
55  
56  /**
57   * <p>
58   * Resolves dependencies for the artifacts in context of the lifecycle build
59   * </p>
60   * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
61   * @since 3.0
62   * @author Benjamin Bentmann
63   * @author Jason van Zyl
64   * @author Kristian Rosenvold (extracted class)
65   */
66  @Named
67  public class LifecycleDependencyResolver {
68  
69      @Inject
70      private ProjectDependenciesResolver dependenciesResolver;
71  
72      @Inject
73      private Logger logger;
74  
75      @Inject
76      private ProjectArtifactFactory artifactFactory;
77  
78      @Inject
79      private EventSpyDispatcher eventSpyDispatcher;
80  
81      @Inject
82      private ProjectArtifactsCache projectArtifactsCache;
83  
84      @Inject
85      private MessageBuilderFactory messageBuilderFactory;
86  
87      public LifecycleDependencyResolver() {}
88  
89      public LifecycleDependencyResolver(ProjectDependenciesResolver projectDependenciesResolver, Logger logger) {
90          this.dependenciesResolver = projectDependenciesResolver;
91          this.logger = logger;
92      }
93  
94      public static List<MavenProject> getProjects(MavenProject project, MavenSession session, boolean aggregator) {
95          if (aggregator && project.getCollectedProjects() != null) {
96              // get the unsorted list of wanted projects
97              Set<MavenProject> projectAndSubmodules = new HashSet<>(project.getCollectedProjects());
98              projectAndSubmodules.add(project);
99              return session.getProjects().stream() // sorted all
100                     .filter(projectAndSubmodules::contains)
101                     .collect(Collectors.toList()); // sorted and filtered to what we need
102         } else {
103             return Collections.singletonList(project);
104         }
105     }
106 
107     public void resolveProjectDependencies(
108             MavenProject project,
109             Collection<String> scopesToCollect,
110             Collection<String> scopesToResolve,
111             MavenSession session,
112             boolean aggregating,
113             Set<Artifact> projectArtifacts)
114             throws LifecycleExecutionException {
115         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
116         try {
117             ClassLoader projectRealm = project.getClassRealm();
118             if (projectRealm != null && projectRealm != tccl) {
119                 Thread.currentThread().setContextClassLoader(projectRealm);
120             }
121 
122             if (project.getDependencyArtifacts() == null) {
123                 try {
124                     project.setDependencyArtifacts(artifactFactory.createArtifacts(project));
125                 } catch (InvalidDependencyVersionException e) {
126                     throw new LifecycleExecutionException(e);
127                 }
128             }
129 
130             ProjectArtifactsCache.Key cacheKey = projectArtifactsCache.createKey(
131                     project, scopesToCollect, scopesToResolve, aggregating, session.getRepositorySession());
132             ProjectArtifactsCache.CacheRecord recordArtifacts;
133             recordArtifacts = projectArtifactsCache.get(cacheKey);
134 
135             if (recordArtifacts == null) {
136                 synchronized (cacheKey) {
137                     recordArtifacts = projectArtifactsCache.get(cacheKey);
138                     if (recordArtifacts == null) {
139                         try {
140                             Set<Artifact> resolvedArtifacts = getDependencies(
141                                     project, scopesToCollect, scopesToResolve, session, aggregating, projectArtifacts);
142                             recordArtifacts = projectArtifactsCache.put(cacheKey, resolvedArtifacts);
143                         } catch (LifecycleExecutionException e) {
144                             projectArtifactsCache.put(cacheKey, e);
145                             projectArtifactsCache.register(project, cacheKey, recordArtifacts);
146                             throw e;
147                         }
148                     }
149                 }
150             }
151             projectArtifactsCache.register(project, cacheKey, recordArtifacts);
152             Set<Artifact> resolvedArtifacts = recordArtifacts.getArtifacts();
153 
154             Map<Artifact, File> reactorProjects =
155                     new HashMap<>(session.getProjects().size());
156             for (MavenProject reactorProject : session.getProjects()) {
157                 reactorProjects.put(
158                         reactorProject.getArtifact(),
159                         reactorProject.getArtifact().getFile());
160             }
161 
162             Map<String, Artifact> map = new HashMap<>();
163             for (Artifact artifact : resolvedArtifacts) {
164                 /**
165                  * MNG-6300: resolvedArtifacts can be cache result; this ensures reactor files are always up to date
166                  * During lifecycle the Artifact.getFile() can change from target/classes to the actual jar.
167                  * This clearly shows that target/classes should not be abused as artifactFile just for the classpath
168                  */
169                 File reactorProjectFile = reactorProjects.get(artifact);
170                 if (reactorProjectFile != null) {
171                     artifact.setFile(reactorProjectFile);
172                 }
173 
174                 map.put(artifact.getDependencyConflictId(), artifact);
175             }
176 
177             project.setResolvedArtifacts(resolvedArtifacts);
178 
179             for (Artifact artifact : project.getDependencyArtifacts()) {
180                 if (artifact.getFile() == null) {
181                     Artifact resolved = map.get(artifact.getDependencyConflictId());
182                     if (resolved != null) {
183                         artifact.setFile(resolved.getFile());
184                         artifact.setDependencyTrail(resolved.getDependencyTrail());
185                         artifact.setResolvedVersion(resolved.getVersion());
186                         artifact.setResolved(true);
187                     }
188                 }
189             }
190         } finally {
191             Thread.currentThread().setContextClassLoader(tccl);
192         }
193     }
194 
195     private Set<Artifact> getDependencies(
196             MavenProject project,
197             Collection<String> scopesToCollect,
198             Collection<String> scopesToResolve,
199             MavenSession session,
200             boolean aggregating,
201             Set<Artifact> projectArtifacts)
202             throws LifecycleExecutionException {
203         if (scopesToCollect == null) {
204             scopesToCollect = Collections.emptySet();
205         }
206         if (scopesToResolve == null) {
207             scopesToResolve = Collections.emptySet();
208         }
209 
210         if (scopesToCollect.isEmpty() && scopesToResolve.isEmpty()) {
211             return new LinkedHashSet<>();
212         }
213 
214         scopesToCollect = new HashSet<>(scopesToCollect);
215         scopesToCollect.addAll(scopesToResolve);
216 
217         DependencyFilter collectionFilter = new ScopeDependencyFilter(null, negate(scopesToCollect));
218         DependencyFilter resolutionFilter = new ScopeDependencyFilter(null, negate(scopesToResolve));
219         resolutionFilter = AndDependencyFilter.newInstance(collectionFilter, resolutionFilter);
220         resolutionFilter =
221                 AndDependencyFilter.newInstance(resolutionFilter, new ReactorDependencyFilter(projectArtifacts));
222 
223         DependencyResolutionResult result;
224         try {
225             DefaultDependencyResolutionRequest request =
226                     new DefaultDependencyResolutionRequest(project, session.getRepositorySession());
227             request.setResolutionFilter(resolutionFilter);
228 
229             eventSpyDispatcher.onEvent(request);
230 
231             result = dependenciesResolver.resolve(request);
232         } catch (DependencyResolutionException e) {
233             result = e.getResult();
234 
235             /*
236              * MNG-2277, the check below compensates for our bad plugin support where we ended up with aggregator
237              * plugins that require dependency resolution although they usually run in phases of the build where project
238              * artifacts haven't been assembled yet. The prime example of this is "mvn release:prepare".
239              */
240             if (aggregating && areAllDependenciesInReactor(session.getProjects(), result.getUnresolvedDependencies())) {
241                 logger.warn("The following dependencies could not be resolved at this point of the build"
242                         + " but seem to be part of the reactor:");
243 
244                 for (Dependency dependency : result.getUnresolvedDependencies()) {
245                     logger.warn("o " + dependency);
246                 }
247 
248                 logger.warn("Try running the build up to the lifecycle phase \"package\"");
249             } else {
250                 throw new LifecycleExecutionException(messageBuilderFactory, null, project, e);
251             }
252         }
253 
254         eventSpyDispatcher.onEvent(result);
255 
256         Set<Artifact> artifacts = new LinkedHashSet<>();
257         if (result.getDependencyGraph() != null
258                 && !result.getDependencyGraph().getChildren().isEmpty()) {
259             RepositoryUtils.toArtifacts(
260                     artifacts,
261                     result.getDependencyGraph().getChildren(),
262                     Collections.singletonList(project.getArtifact().getId()),
263                     collectionFilter);
264         }
265         return artifacts;
266     }
267 
268     private boolean areAllDependenciesInReactor(
269             Collection<MavenProject> projects, Collection<Dependency> dependencies) {
270         Set<String> projectKeys = getReactorProjectKeys(projects);
271 
272         for (Dependency dependency : dependencies) {
273             org.eclipse.aether.artifact.Artifact a = dependency.getArtifact();
274             String key = ArtifactUtils.key(a.getGroupId(), a.getArtifactId(), a.getVersion());
275             if (!projectKeys.contains(key)) {
276                 return false;
277             }
278         }
279 
280         return true;
281     }
282 
283     private Set<String> getReactorProjectKeys(Collection<MavenProject> projects) {
284         Set<String> projectKeys = new HashSet<>(projects.size() * 2);
285         for (MavenProject project : projects) {
286             String key = ArtifactUtils.key(project.getGroupId(), project.getArtifactId(), project.getVersion());
287             projectKeys.add(key);
288         }
289         return projectKeys;
290     }
291 
292     private Collection<String> negate(Collection<String> scopes) {
293         Collection<String> result = new HashSet<>();
294         Collections.addAll(result, "system", "compile", "provided", "runtime", "test");
295 
296         for (String scope : scopes) {
297             if ("compile".equals(scope)) {
298                 result.remove("compile");
299                 result.remove("system");
300                 result.remove("provided");
301             } else if ("runtime".equals(scope)) {
302                 result.remove("compile");
303                 result.remove("runtime");
304             } else if ("compile+runtime".equals(scope)) {
305                 result.remove("compile");
306                 result.remove("system");
307                 result.remove("provided");
308                 result.remove("runtime");
309             } else if ("runtime+system".equals(scope)) {
310                 result.remove("compile");
311                 result.remove("system");
312                 result.remove("runtime");
313             } else if ("test".equals(scope)) {
314                 result.clear();
315             }
316         }
317 
318         return result;
319     }
320 
321     private static class ReactorDependencyFilter implements DependencyFilter {
322 
323         private Set<String> keys = new HashSet<>();
324 
325         ReactorDependencyFilter(Collection<Artifact> artifacts) {
326             for (Artifact artifact : artifacts) {
327                 String key = ArtifactUtils.key(artifact);
328                 keys.add(key);
329             }
330         }
331 
332         public boolean accept(DependencyNode node, List<DependencyNode> parents) {
333             Dependency dependency = node.getDependency();
334             if (dependency != null) {
335                 org.eclipse.aether.artifact.Artifact a = dependency.getArtifact();
336                 String key = ArtifactUtils.key(a.getGroupId(), a.getArtifactId(), a.getVersion());
337                 return !keys.contains(key);
338             }
339             return false;
340         }
341     }
342 }