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