1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.basepom.mojo.dvc.dependency;
16
17 import static com.google.common.base.Preconditions.checkNotNull;
18 import static com.google.common.base.Preconditions.checkState;
19 import static com.google.common.collect.ImmutableList.toImmutableList;
20 import static com.google.common.collect.ImmutableSet.toImmutableSet;
21 import static java.lang.String.format;
22 import static org.basepom.mojo.dvc.dependency.DependencyMapBuilder.convertToPomArtifact;
23
24 import org.basepom.mojo.dvc.CheckExclusionsFilter;
25 import org.basepom.mojo.dvc.Context;
26 import org.basepom.mojo.dvc.PluginLog;
27 import org.basepom.mojo.dvc.QualifiedName;
28 import org.basepom.mojo.dvc.ScopeLimitingFilter;
29 import org.basepom.mojo.dvc.strategy.Strategy;
30 import org.basepom.mojo.dvc.version.VersionResolution;
31 import org.basepom.mojo.dvc.version.VersionResolutionCollection;
32
33 import java.time.Duration;
34 import java.util.Collection;
35 import java.util.Set;
36 import java.util.concurrent.Callable;
37 import java.util.concurrent.ExecutionException;
38 import java.util.concurrent.Executors;
39 import java.util.concurrent.locks.Lock;
40 import java.util.concurrent.locks.ReentrantLock;
41
42 import com.google.common.base.Joiner;
43 import com.google.common.collect.ImmutableList;
44 import com.google.common.collect.ImmutableSet;
45 import com.google.common.collect.ImmutableSetMultimap;
46 import com.google.common.util.concurrent.ListenableFuture;
47 import com.google.common.util.concurrent.ListeningExecutorService;
48 import com.google.common.util.concurrent.MoreExecutors;
49 import com.google.common.util.concurrent.ThreadFactoryBuilder;
50 import org.apache.maven.RepositoryUtils;
51 import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
52 import org.apache.maven.artifact.versioning.ComparableVersion;
53 import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
54 import org.apache.maven.plugin.MojoExecutionException;
55 import org.apache.maven.project.DependencyResolutionException;
56 import org.apache.maven.project.MavenProject;
57 import org.apache.maven.project.ProjectBuildingException;
58 import org.eclipse.aether.RepositorySystem;
59 import org.eclipse.aether.artifact.Artifact;
60 import org.eclipse.aether.artifact.ArtifactTypeRegistry;
61 import org.eclipse.aether.graph.Dependency;
62 import org.eclipse.aether.graph.DependencyFilter;
63 import org.eclipse.aether.graph.DependencyNode;
64 import org.eclipse.aether.resolution.VersionRangeRequest;
65 import org.eclipse.aether.resolution.VersionRangeResolutionException;
66 import org.eclipse.aether.resolution.VersionRangeResult;
67 import org.eclipse.aether.util.filter.AndDependencyFilter;
68
69 public final class DependencyTreeResolver
70 implements AutoCloseable {
71
72 private static final PluginLog LOG = new PluginLog(DependencyTreeResolver.class);
73
74 private static final int DEPENDENCY_RESOLUTION_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 5;
75
76 private final Lock collectorLock = new ReentrantLock();
77
78 private final Context context;
79 private final DependencyMap rootDependencyMap;
80
81 private final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(DEPENDENCY_RESOLUTION_NUM_THREADS,
82 new ThreadFactoryBuilder().setNameFormat("dependency-version-check-worker-%s").setDaemon(true).build()));
83
84 public DependencyTreeResolver(final Context context, final DependencyMap rootDependencyMap) {
85 this.context = checkNotNull(context, "context is null");
86 this.rootDependencyMap = checkNotNull(rootDependencyMap, "rootDependencyMap is null");
87 }
88
89 @Override
90 public void close() {
91 MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(2));
92 }
93
94
95
96
97
98
99
100
101
102
103
104
105 public ImmutableSetMultimap<QualifiedName, VersionResolutionCollection> computeResolutionMap(final MavenProject project,
106 final ScopeLimitingFilter scopeFilter)
107 throws MojoExecutionException {
108 checkNotNull(project, "project is null");
109 checkNotNull(scopeFilter, "scope is null");
110
111 final ImmutableSetMultimap.Builder<QualifiedName, VersionResolution> collector = ImmutableSetMultimap.builder();
112 final ImmutableList.Builder<ListenableFuture<?>> futureBuilder = ImmutableList.builder();
113
114 boolean useParallelDependencyResolution = context.useFastResolution();
115
116 LOG.debug("Using parallel dependency resolution: %s", useParallelDependencyResolution);
117
118 final ImmutableList<Dependency> dependencies;
119 if (context.useDeepScan()) {
120 LOG.debug("Running deep scan");
121 dependencies = ImmutableList.copyOf(
122 rootDependencyMap.getAllDependencies().values().stream().map(DependencyNode::getDependency).collect(toImmutableList()));
123 } else {
124 final ArtifactTypeRegistry stereotypes = context.getRepositorySystemSession().getArtifactTypeRegistry();
125
126 dependencies = ImmutableList.copyOf(
127 project.getDependencies().stream().map(d -> RepositoryUtils.toDependency(d, stereotypes)).collect(toImmutableList()));
128 }
129
130 final ImmutableSet.Builder<Throwable> throwableBuilder = ImmutableSet.builder();
131
132 if (useParallelDependencyResolution) {
133 for (final Dependency dependency : dependencies) {
134 futureBuilder.add(executorService.submit((Callable<Void>) () -> {
135 resolveProjectDependency(dependency, scopeFilter, collector);
136 return null;
137 }));
138 }
139
140 final ImmutableList<ListenableFuture<?>> futures = futureBuilder.build();
141
142 for (final ListenableFuture<?> future : futures) {
143 try {
144 future.get();
145 } catch (InterruptedException e) {
146 Thread.currentThread().interrupt();
147 } catch (ExecutionException e) {
148 throwableBuilder.add(e.getCause());
149 }
150 }
151 } else {
152 for (final Dependency dependency : dependencies) {
153 try {
154 resolveProjectDependency(dependency, scopeFilter, collector);
155 } catch (Exception e) {
156 throwableBuilder.add(e);
157 }
158 }
159 }
160
161 final Set<Throwable> throwables = throwableBuilder.build();
162 if (!throwables.isEmpty()) {
163 throw processResolveProjectDependencyException(throwableBuilder.build());
164 }
165
166 return VersionResolutionCollection.toResolutionMap(collector.build());
167 }
168
169 private static MojoExecutionException processResolveProjectDependencyException(Set<Throwable> throwables) {
170 ImmutableSet.Builder<String> failedDependenciesBuilder = ImmutableSet.builder();
171 ImmutableSet.Builder<String> messageBuilder = ImmutableSet.builder();
172 for (Throwable t : throwables) {
173 if (t instanceof DependencyResolutionException) {
174 ((DependencyResolutionException) t).getResult().getUnresolvedDependencies()
175 .forEach(d -> failedDependenciesBuilder.add(printDependency(d)));
176 } else {
177 messageBuilder.add(t.getMessage());
178 }
179 }
180
181 String message = Joiner.on(" \n").join(messageBuilder.build());
182 Set<String> failedDependencies = failedDependenciesBuilder.build();
183 if (!failedDependencies.isEmpty()) {
184 if (!message.isEmpty()) {
185 message += "\n";
186 }
187 message += "Could not resolve dependencies: [" + Joiner.on(", ").join(failedDependencies) + "]";
188 }
189 return new MojoExecutionException(message);
190 }
191
192 private static String printDependency(Dependency d) {
193 return d.getArtifact() + " [" + d.getScope() + (d.isOptional() ? ", optional" : "") + "]";
194 }
195
196
197
198
199
200 private void resolveProjectDependency(final Dependency dependency,
201 final ScopeLimitingFilter visibleScopes,
202 final ImmutableSetMultimap.Builder<QualifiedName, VersionResolution> collector)
203 throws MojoExecutionException, DependencyResolutionException, AbstractArtifactResolutionException, VersionRangeResolutionException {
204 final QualifiedName dependencyName = QualifiedName.fromDependency(dependency);
205
206
207
208
209 if (rootDependencyMap.getDirectDependencies().containsKey(dependencyName)) {
210
211 final DependencyNode projectDependencyNode = rootDependencyMap.getDirectDependencies().get(dependencyName);
212 assert projectDependencyNode != null;
213
214 checkState(visibleScopes.accept(projectDependencyNode, ImmutableList.of()),
215 "Dependency %s maps to %s, but scope filter would exclude it. This should never happen!", dependency, projectDependencyNode);
216 computeVersionResolutionForDirectDependency(collector, dependency, projectDependencyNode);
217 }
218
219
220 final DependencyNode projectDependencyNode = rootDependencyMap.getAllDependencies().get(dependencyName);
221
222
223 if (projectDependencyNode == null) {
224 return;
225 }
226 checkState(visibleScopes.accept(projectDependencyNode, ImmutableList.of()),
227 "Dependency %s maps to %s, but scope filter would exclude it. This should never happen!", dependency, projectDependencyNode);
228
229 try {
230
231
232 final ScopeLimitingFilter dependencyScope = ScopeLimitingFilter.computeTransitiveScope(dependency.getScope());
233 computeVersionResolutionForTransitiveDependencies(collector, dependency, projectDependencyNode, dependencyScope);
234 } catch (ProjectBuildingException e) {
235
236
237
238
239 if (projectDependencyNode.getChildren().isEmpty()) {
240 LOG.debug("Ignoring model building exception for %s, no children were declared", dependency);
241 } else {
242 LOG.warn("Could not read POM for %s, ignoring project and its dependencies!", dependency);
243 }
244 }
245 }
246
247
248
249
250 private void computeVersionResolutionForDirectDependency(
251 final ImmutableSetMultimap.Builder<QualifiedName, VersionResolution> collector,
252 final Dependency requestingDependency,
253 final DependencyNode resolvedDependencyNode)
254 throws AbstractArtifactResolutionException, VersionRangeResolutionException, MojoExecutionException {
255 final QualifiedName requestingDependencyName = QualifiedName.fromDependency(requestingDependency);
256
257 final RepositorySystem repoSystem = context.getRepositorySystem();
258
259 Artifact artifact = convertToPomArtifact(requestingDependency.getArtifact());
260 if (artifact.isSnapshot()) {
261
262
263 artifact = artifact.setVersion(artifact.getBaseVersion());
264 }
265
266 final VersionRangeRequest request = context.createVersionRangeRequest(artifact);
267 final VersionRangeResult result = repoSystem.resolveVersionRange(context.getRepositorySystemSession(), request);
268
269 if (!result.getVersions().contains(resolvedDependencyNode.getVersion())) {
270 throw new MojoExecutionException(
271 format("Cannot determine the recommended version of dependency '%s'; its version specification is '%s', and the resolved version is '%s'.",
272 requestingDependency, requestingDependency.getArtifact().getBaseVersion(), resolvedDependencyNode.getVersion()));
273 }
274
275
276
277
278 final ComparableVersion expectedVersion = getVersion(resolvedDependencyNode);
279
280
281 final boolean managedDependency = (resolvedDependencyNode.getManagedBits() & DependencyNode.MANAGED_VERSION) != 0;
282 final VersionResolution resolution = VersionResolution.forDirectDependency(QualifiedName.fromProject(context.getRootProject()), expectedVersion,
283 managedDependency);
284
285 if (isIncluded(resolvedDependencyNode, expectedVersion, expectedVersion)) {
286 final Strategy strategy = context.getStrategyCache().forQualifiedName(requestingDependencyName);
287 checkState(strategy != null, "Strategy for %s is null, this should never happen (could not find default strategy?", requestingDependencyName);
288
289 if (!strategy.isCompatible(expectedVersion, expectedVersion)) {
290 resolution.conflict();
291 }
292 } else {
293 LOG.debug("VersionResolution %s is excluded by configuration.", resolution);
294 }
295
296 try {
297 collectorLock.lock();
298 collector.put(requestingDependencyName, resolution);
299 } finally {
300 collectorLock.unlock();
301 }
302 }
303
304
305
306
307
308 private void computeVersionResolutionForTransitiveDependencies(
309 final ImmutableSetMultimap.Builder<QualifiedName, VersionResolution> collector,
310 final Dependency requestingDependency,
311 final DependencyNode dependencyNodeForDependency,
312 final DependencyFilter scopeFilter)
313 throws AbstractArtifactResolutionException, ProjectBuildingException, DependencyResolutionException {
314 final AndDependencyFilter filter = new AndDependencyFilter(scopeFilter, new CheckExclusionsFilter(requestingDependency.getExclusions()));
315
316 final DependencyMap dependencyMap = new DependencyMapBuilder(context).mapDependency(dependencyNodeForDependency, filter);
317 final Collection<DependencyNode> transitiveDependencies = dependencyMap.getAllDependencies().values();
318 final QualifiedName requestingDependencyName = QualifiedName.fromDependency(requestingDependency);
319
320 final ImmutableSet<DependencyNode> filteredDependencies = transitiveDependencies.stream()
321 .filter(d -> scopeFilter.accept(d, ImmutableList.of()))
322 .filter(d -> !d.getDependency().isOptional())
323 .collect(toImmutableSet());
324
325 for (final DependencyNode dependencyNode : filteredDependencies) {
326 final QualifiedName dependencyName = QualifiedName.fromDependencyNode(dependencyNode);
327
328 final DependencyNode projectDependencyNode = rootDependencyMap.getAllDependencies().get(dependencyName);
329 if (projectDependencyNode == null) {
330
331
332
333
334
335
336
337
338
339
340
341
342
343 continue;
344 }
345
346 final ComparableVersion resolvedVersion = getVersion(projectDependencyNode);
347 final ComparableVersion expectedVersion = getVersion(dependencyNode);
348
349 final boolean managedDependency = (projectDependencyNode.getManagedBits() & DependencyNode.MANAGED_VERSION) != 0;
350 final VersionResolution resolution = VersionResolution.forTransitiveDependency(requestingDependencyName, expectedVersion, managedDependency);
351
352 if (isIncluded(dependencyNode, expectedVersion, resolvedVersion)) {
353 final Strategy strategy = context.getStrategyCache().forQualifiedName(dependencyName);
354 checkState(strategy != null, "Strategy for %s is null, this should never happen (could not find default strategy?", dependencyName);
355
356 if (!strategy.isCompatible(expectedVersion, resolvedVersion)) {
357 resolution.conflict();
358 }
359 }
360
361 try {
362 collectorLock.lock();
363 collector.put(dependencyName, resolution);
364 } finally {
365 collectorLock.unlock();
366 }
367 }
368 }
369
370
371
372
373 private boolean isIncluded(DependencyNode dependencyNodeForDependency, ComparableVersion expectedVersion, ComparableVersion resolvedVersion) {
374 return context.getExclusions().stream().noneMatch(exclusion -> exclusion.matches(dependencyNodeForDependency, expectedVersion, resolvedVersion));
375 }
376
377
378
379
380 private static ComparableVersion getVersion(DependencyNode dependencyNode)
381 throws OverConstrainedVersionException {
382 checkNotNull(dependencyNode, "dependencyNode is null");
383
384 checkState(dependencyNode.getVersion() != null, "DependencyNode %s has a null version selected. Please report a bug!", dependencyNode);
385 return new ComparableVersion(dependencyNode.getVersion().toString());
386 }
387 }