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  
34  import org.apache.maven.RepositoryUtils;
35  import org.apache.maven.artifact.Artifact;
36  import org.apache.maven.artifact.ArtifactUtils;
37  import org.apache.maven.eventspy.internal.EventSpyDispatcher;
38  import org.apache.maven.execution.MavenSession;
39  import org.apache.maven.lifecycle.LifecycleExecutionException;
40  import org.apache.maven.project.DefaultDependencyResolutionRequest;
41  import org.apache.maven.project.DependencyResolutionException;
42  import org.apache.maven.project.DependencyResolutionResult;
43  import org.apache.maven.project.MavenProject;
44  import org.apache.maven.project.ProjectDependenciesResolver;
45  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
46  import org.apache.maven.project.artifact.ProjectArtifactsCache;
47  import org.eclipse.aether.graph.Dependency;
48  import org.eclipse.aether.graph.DependencyFilter;
49  import org.eclipse.aether.graph.DependencyNode;
50  import org.eclipse.aether.util.filter.AndDependencyFilter;
51  import org.eclipse.aether.util.filter.ScopeDependencyFilter;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  /**
56   * <p>
57   * Resolves dependencies for the artifacts in context of the lifecycle build
58   * </p>
59   * <strong>NOTE:</strong> This class is not part of any public api and can be changed or deleted without prior notice.
60   * @since 3.0
61   * @author Benjamin Bentmann
62   * @author Jason van Zyl
63   * @author Kristian Rosenvold (extracted class)
64   */
65  @Named
66  public class LifecycleDependencyResolver {
67      private final Logger logger = LoggerFactory.getLogger(getClass());
68  
69      private final ProjectDependenciesResolver dependenciesResolver;
70  
71      private final ProjectArtifactFactory artifactFactory;
72  
73      private final EventSpyDispatcher eventSpyDispatcher;
74  
75      private final ProjectArtifactsCache projectArtifactsCache;
76  
77      @Inject
78      public LifecycleDependencyResolver(
79              ProjectDependenciesResolver dependenciesResolver,
80              ProjectArtifactFactory artifactFactory,
81              EventSpyDispatcher eventSpyDispatcher,
82              ProjectArtifactsCache projectArtifactsCache) {
83          this.dependenciesResolver = dependenciesResolver;
84          this.artifactFactory = artifactFactory;
85          this.eventSpyDispatcher = eventSpyDispatcher;
86          this.projectArtifactsCache = projectArtifactsCache;
87      }
88  
89      public static List<MavenProject> getProjects(MavenProject project, MavenSession session, boolean aggregator) {
90          if (aggregator) {
91              return session.getProjects();
92          } else {
93              return Collections.singletonList(project);
94          }
95      }
96  
97      public void resolveProjectDependencies(
98              MavenProject project,
99              Collection<String> scopesToCollect,
100             Collection<String> scopesToResolve,
101             MavenSession session,
102             boolean aggregating,
103             Set<Artifact> projectArtifacts)
104             throws LifecycleExecutionException {
105         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
106         try {
107             ClassLoader projectRealm = project.getClassRealm();
108             if (projectRealm != null && projectRealm != tccl) {
109                 Thread.currentThread().setContextClassLoader(projectRealm);
110             }
111 
112             if (project.getDependencyArtifacts() == null) {
113                 try {
114                     project.setDependencyArtifacts(artifactFactory.createArtifacts(project));
115                 } catch (InvalidDependencyVersionException e) {
116                     throw new LifecycleExecutionException(e);
117                 }
118             }
119 
120             Set<Artifact> resolvedArtifacts = resolveProjectArtifacts(
121                     project, scopesToCollect, scopesToResolve, session, aggregating, projectArtifacts);
122 
123             Map<Artifact, File> reactorProjects =
124                     new HashMap<>(session.getProjects().size());
125             for (MavenProject reactorProject : session.getProjects()) {
126                 reactorProjects.put(
127                         reactorProject.getArtifact(),
128                         reactorProject.getArtifact().getFile());
129             }
130 
131             Map<String, Artifact> map = new HashMap<>();
132             for (Artifact artifact : resolvedArtifacts) {
133                 /**
134                  * MNG-6300: resolvedArtifacts can be cache result; this ensures reactor files are always up-to-date
135                  * During lifecycle the Artifact.getFile() can change from target/classes to the actual jar.
136                  * This clearly shows that target/classes should not be abused as artifactFile just for the classpath
137                  */
138                 File reactorProjectFile = reactorProjects.get(artifact);
139                 if (reactorProjectFile != null) {
140                     artifact.setFile(reactorProjectFile);
141                 }
142 
143                 map.put(artifact.getDependencyConflictId(), artifact);
144             }
145 
146             project.setResolvedArtifacts(resolvedArtifacts);
147 
148             for (Artifact artifact : project.getDependencyArtifacts()) {
149                 if (artifact.getFile() == null) {
150                     Artifact resolved = map.get(artifact.getDependencyConflictId());
151                     if (resolved != null) {
152                         artifact.setFile(resolved.getFile());
153                         artifact.setDependencyTrail(resolved.getDependencyTrail());
154                         artifact.setResolvedVersion(resolved.getVersion());
155                         artifact.setResolved(true);
156                     }
157                 }
158             }
159         } finally {
160             Thread.currentThread().setContextClassLoader(tccl);
161         }
162     }
163 
164     public Set<Artifact> resolveProjectArtifacts(
165             MavenProject project,
166             Collection<String> scopesToCollect,
167             Collection<String> scopesToResolve,
168             MavenSession session,
169             boolean aggregating,
170             Set<Artifact> projectArtifacts)
171             throws LifecycleExecutionException {
172         Set<Artifact> resolvedArtifacts;
173         ProjectArtifactsCache.Key cacheKey = projectArtifactsCache.createKey(
174                 project, scopesToCollect, scopesToResolve, aggregating, session.getRepositorySession());
175         ProjectArtifactsCache.CacheRecord recordArtifacts;
176         recordArtifacts = projectArtifactsCache.get(cacheKey);
177 
178         if (recordArtifacts != null) {
179             resolvedArtifacts = recordArtifacts.getArtifacts();
180         } else {
181             try {
182                 resolvedArtifacts = getDependencies(
183                         project, scopesToCollect, scopesToResolve, session, aggregating, projectArtifacts);
184                 recordArtifacts = projectArtifactsCache.put(cacheKey, resolvedArtifacts);
185             } catch (LifecycleExecutionException e) {
186                 projectArtifactsCache.put(cacheKey, e);
187                 projectArtifactsCache.register(project, cacheKey, recordArtifacts);
188                 throw e;
189             }
190         }
191         projectArtifactsCache.register(project, cacheKey, recordArtifacts);
192         return resolvedArtifacts;
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(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 }