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.eclipse.aether.util.graph.transformer;
20
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.stream.Collectors;
30
31 import org.eclipse.aether.RepositoryException;
32 import org.eclipse.aether.artifact.Artifact;
33 import org.eclipse.aether.collection.DependencyGraphTransformationContext;
34 import org.eclipse.aether.graph.DefaultDependencyNode;
35 import org.eclipse.aether.graph.Dependency;
36 import org.eclipse.aether.graph.DependencyNode;
37 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
38
39 import static java.util.Objects.requireNonNull;
40
41 /**
42 * A high-performance dependency graph transformer that resolves version and scope conflicts among dependencies.
43 * This is the recommended conflict resolver implementation that provides O(N) performance characteristics,
44 * significantly improving upon the O(N²) worst-case performance of {@link ClassicConflictResolver}.
45 * <p>
46 * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
47 * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
48 * be left in place with conflict information. The exact rules by which a winning node and its effective scope
49 * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
50 * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
51 * <p>
52 * <strong>Performance Characteristics:</strong>
53 * <ul>
54 * <li><strong>Time Complexity:</strong> O(N) where N is the number of dependency nodes</li>
55 * <li><strong>Memory Usage:</strong> Creates a parallel tree structure for conflict-free processing</li>
56 * <li><strong>Scalability:</strong> Excellent performance on large multi-module projects</li>
57 * </ul>
58 * <p>
59 * <strong>Algorithm Overview:</strong>
60 * <ol>
61 * <li><strong>Path Tree Construction:</strong> Builds a cycle-free parallel tree structure from the input
62 * dependency graph, where each {@code Path} represents a unique route to a dependency node</li>
63 * <li><strong>Conflict Partitioning:</strong> Groups paths by conflict ID (based on groupId:artifactId:classifier:extension coordinates)</li>
64 * <li><strong>Topological Processing:</strong> Processes conflict groups in topologically sorted order</li>
65 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners within each conflict group</li>
66 * <li><strong>Graph Transformation:</strong> Applies changes back to the original dependency graph</li>
67 * </ol>
68 * <p>
69 * <strong>Key Differences from {@link ClassicConflictResolver}:</strong>
70 * <ul>
71 * <li><strong>Performance:</strong> O(N) vs O(N²) time complexity</li>
72 * <li><strong>Memory Strategy:</strong> Uses parallel tree structure vs in-place graph modification</li>
73 * <li><strong>Cycle Handling:</strong> Explicitly breaks cycles during tree construction</li>
74 * <li><strong>Processing Order:</strong> Level-by-level from root vs depth-first traversal</li>
75 * </ul>
76 * <p>
77 * <strong>When to Use:</strong>
78 * <ul>
79 * <li>Default choice for all new projects and Maven 4+ installations</li>
80 * <li>Large multi-module projects with many dependencies</li>
81 * <li>Performance-critical build environments</li>
82 * <li>Any scenario where {@link ClassicConflictResolver} shows performance bottlenecks</li>
83 * </ul>
84 * <p>
85 * <strong>Implementation Note:</strong> This conflict resolver builds a cycle-free "parallel" structure based on the
86 * passed-in dependency graph, and applies operations level by level starting from the root. The parallel {@code Path}
87 * tree ensures that cycles in the original graph don't affect the conflict resolution algorithm's performance.
88 *
89 * @see ClassicConflictResolver
90 * @since 2.0.11
91 */
92 public final class PathConflictResolver extends ConflictResolver {
93 private final ConflictResolver.VersionSelector versionSelector;
94 private final ConflictResolver.ScopeSelector scopeSelector;
95 private final ConflictResolver.ScopeDeriver scopeDeriver;
96 private final ConflictResolver.OptionalitySelector optionalitySelector;
97
98 /**
99 * Creates a new conflict resolver instance with the specified hooks.
100 *
101 * @param versionSelector The version selector to use, must not be {@code null}.
102 * @param scopeSelector The scope selector to use, must not be {@code null}.
103 * @param optionalitySelector The optionality selector ot use, must not be {@code null}.
104 * @param scopeDeriver The scope deriver to use, must not be {@code null}.
105 */
106 public PathConflictResolver(
107 ConflictResolver.VersionSelector versionSelector,
108 ConflictResolver.ScopeSelector scopeSelector,
109 ConflictResolver.OptionalitySelector optionalitySelector,
110 ConflictResolver.ScopeDeriver scopeDeriver) {
111 this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null");
112 this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null");
113 this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null");
114 this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null");
115 }
116
117 @SuppressWarnings("unchecked")
118 @Override
119 public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
120 throws RepositoryException {
121 requireNonNull(node, "node cannot be null");
122 requireNonNull(context, "context cannot be null");
123 List<String> sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
124 if (sortedConflictIds == null) {
125 ConflictIdSorter sorter = new ConflictIdSorter();
126 sorter.transformGraph(node, context);
127
128 sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
129 }
130
131 @SuppressWarnings("unchecked")
132 Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
133 long time1 = System.nanoTime();
134
135 @SuppressWarnings("unchecked")
136 Collection<Collection<String>> conflictIdCycles =
137 (Collection<Collection<String>>) context.get(TransformationContextKeys.CYCLIC_CONFLICT_IDS);
138 if (conflictIdCycles == null) {
139 throw new RepositoryException("conflict id cycles have not been identified");
140 }
141
142 Map<DependencyNode, String> conflictIds =
143 (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
144 if (conflictIds == null) {
145 throw new RepositoryException("conflict groups have not been identified");
146 }
147
148 Map<String, Collection<String>> cyclicPredecessors = new HashMap<>();
149 for (Collection<String> cycle : conflictIdCycles) {
150 for (String conflictId : cycle) {
151 Collection<String> predecessors = cyclicPredecessors.computeIfAbsent(conflictId, k -> new HashSet<>());
152 predecessors.addAll(cycle);
153 }
154 }
155
156 State state = new State(
157 ConflictResolver.getVerbosity(context.getSession()),
158 versionSelector.getInstance(node, context),
159 scopeSelector.getInstance(node, context),
160 scopeDeriver.getInstance(node, context),
161 optionalitySelector.getInstance(node, context),
162 sortedConflictIds,
163 conflictIds,
164 cyclicPredecessors);
165
166 state.build(node);
167
168 // loop over topographically sorted conflictIds
169 for (String conflictId : state.sortedConflictIds) {
170 // paths in given conflict group to consider
171 List<Path> paths = state.partitions.get(conflictId);
172 if (paths.isEmpty()) {
173 // this means that whole group "fall out of scope" (are all on loser branches); skip
174 continue;
175 }
176
177 // create conflict context for given conflictId
178 ConflictContext ctx = new ConflictContext(
179 node,
180 state.conflictIds,
181 paths.stream().map(ConflictItem::new).collect(Collectors.toList()),
182 conflictId);
183
184 // select winner (is done by VersionSelector)
185 state.versionSelector.selectVersion(ctx);
186 if (ctx.winner == null) {
187 throw new RepositoryException("conflict resolver did not select winner among " + ctx.items);
188 }
189 // select scope (no side effect between this and above operations)
190 state.scopeSelector.selectScope(ctx);
191 // select optionality (no side effect between this and above operations)
192 state.optionalitySelector.selectOptionality(ctx);
193
194 // we have a winner path
195 Path winnerPath = ctx.winner.path;
196
197 // mark conflictId as resolved with winner; sanity check
198 if (state.resolvedIds.containsKey(conflictId)) {
199 throw new RepositoryException("conflict resolver already have winner for conflictId=" + conflictId
200 + ": " + state.resolvedIds);
201 }
202 state.resolvedIds.put(conflictId, winnerPath);
203
204 // loop over considered paths and apply selection results; note: node may remove itself from iterated list
205 for (Path path : new ArrayList<>(paths)) {
206 // apply selected inherited properties scope/optional to all (winner carries version; others are losers)
207 path.scope = ctx.scope;
208 path.optional = ctx.optional;
209
210 // reset children as inheritance may be affected by this node scope/optionality change
211 path.children.forEach(c -> c.pull(0));
212 // derive with new values from this to children only; observe winner flag
213 path.derive(1, path == winnerPath);
214 // push this node full level changes to DN graph
215 path.push(0);
216 }
217 }
218
219 if (stats != null) {
220 long time2 = System.nanoTime();
221 stats.put("ConflictResolver.totalTime", time2 - time1);
222 stats.put(
223 "ConflictResolver.conflictItemCount",
224 state.partitions.values().stream().map(List::size).reduce(0, Integer::sum));
225 }
226
227 return node;
228 }
229
230 /**
231 * State of conflict resolution processing, to make this component (held in session) re-entrant by multiple threads.
232 */
233 private static class State {
234 /**
235 * Verbosity to be applied, see {@link ConflictResolver.Verbosity}.
236 */
237 private final ConflictResolver.Verbosity verbosity;
238
239 /**
240 * The {@link ConflictResolver.VersionSelector} to use.
241 */
242 private final ConflictResolver.VersionSelector versionSelector;
243
244 /**
245 * The {@link ConflictResolver.ScopeSelector} to use.
246 */
247 private final ConflictResolver.ScopeSelector scopeSelector;
248
249 /**
250 * The {@link ConflictResolver.ScopeDeriver} to use.
251 */
252 private final ConflictResolver.ScopeDeriver scopeDeriver;
253
254 /**
255 * The {@link ConflictResolver.OptionalitySelector} to use/
256 */
257 private final ConflictResolver.OptionalitySelector optionalitySelector;
258
259 /**
260 * Topologically sorted conflictIds from {@link ConflictIdSorter}.
261 */
262 private final List<String> sortedConflictIds;
263
264 /**
265 * The node to conflictId mapping from {@link ConflictMarker}.
266 */
267 private final Map<DependencyNode, String> conflictIds;
268
269 /**
270 * The map of conflictIds which could apply to ancestors of nodes with the key conflict id, used to avoid
271 * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account
272 * for cyclic dependencies. From {@link ConflictIdSorter}.
273 */
274 private final Map<String, Collection<String>> cyclicPredecessors;
275
276 /**
277 * A mapping from conflictId to paths represented as {@link Path}s that exist for each conflictId. In other
278 * words all paths to each {@link DependencyNode} that are member of same conflictId group.
279 */
280 private final Map<String, List<Path>> partitions;
281
282 /**
283 * A mapping from conflictIds to winner {@link Path}, hence {@link DependencyNode} for given conflictId.
284 */
285 private final Map<String, Path> resolvedIds;
286
287 @SuppressWarnings("checkstyle:ParameterNumber")
288 private State(
289 ConflictResolver.Verbosity verbosity,
290 ConflictResolver.VersionSelector versionSelector,
291 ConflictResolver.ScopeSelector scopeSelector,
292 ConflictResolver.ScopeDeriver scopeDeriver,
293 ConflictResolver.OptionalitySelector optionalitySelector,
294 List<String> sortedConflictIds,
295 Map<DependencyNode, String> conflictIds,
296 Map<String, Collection<String>> cyclicPredecessors) {
297 this.verbosity = verbosity;
298 this.versionSelector = versionSelector;
299 this.scopeSelector = scopeSelector;
300 this.scopeDeriver = scopeDeriver;
301 this.optionalitySelector = optionalitySelector;
302 this.sortedConflictIds = sortedConflictIds;
303 this.conflictIds = conflictIds;
304 this.cyclicPredecessors = cyclicPredecessors;
305 this.partitions = new HashMap<>();
306 this.resolvedIds = new HashMap<>();
307 }
308
309 /**
310 * Consumes the dirty graph and builds internal structures out of {@link Path} instances that is always a
311 * tree.
312 */
313 private Path build(DependencyNode node) throws RepositoryException {
314 Path root = new Path(this, node, null);
315 gatherCRNodes(root);
316 return root;
317 }
318
319 /**
320 * Recursively builds {@link Path} graph by observing each node associated {@link DependencyNode}.
321 */
322 private void gatherCRNodes(Path node) throws RepositoryException {
323 List<DependencyNode> children = node.dn.getChildren();
324 if (!children.isEmpty()) {
325 // add children; we will get back those really added (not causing cycles)
326 List<Path> added = node.addChildren(children);
327 for (Path child : added) {
328 gatherCRNodes(child);
329 }
330 }
331 }
332 }
333
334 /**
335 * Represents a unique path within the dependency graph from the root to a specific {@link DependencyNode}.
336 * This is the core data structure that enables the O(N) performance of {@link PathConflictResolver}.
337 * <p>
338 * <strong>Key Concepts:</strong>
339 * <ul>
340 * <li><strong>Path Uniqueness:</strong> Each {@code Path} instance represents a distinct route through
341 * the dependency graph, even if multiple paths lead to the same {@code DependencyNode}</li>
342 * <li><strong>Cycle-Free Structure:</strong> The {@code Path} tree is guaranteed to be acyclic, even
343 * when the original dependency graph contains cycles</li>
344 * <li><strong>Parallel Structure:</strong> This creates a "clean" tree alongside the original "dirty"
345 * graph for efficient processing</li>
346 * </ul>
347 * <p>
348 * <strong>Example:</strong> If dependency A appears in the graph via two different routes:
349 * <pre>
350 * Root → B → A (path 1)
351 * Root → C → A (path 2)
352 * </pre>
353 * Two separate {@code Path} instances will be created, both pointing to the same {@code DependencyNode} A,
354 * but representing different paths through the dependency tree.
355 * <p>
356 * <strong>Memory Optimization:</strong> While this creates additional objects, it enables the algorithm
357 * to process conflicts in O(N) time rather than O(N²), making it much more efficient for large graphs.
358 * <p>
359 * <strong>Conflict Resolution:</strong> Paths are grouped by conflict ID (based on groupId:artifactId:classifier:extension coordinates),
360 * and the conflict resolution algorithm can efficiently process each group independently.
361 */
362 private static class Path {
363 private final State state;
364 private DependencyNode dn;
365 private final String conflictId;
366 private final Path parent;
367 private final int depth;
368 private final List<Path> children;
369 private String scope;
370 private boolean optional;
371
372 private Path(State state, DependencyNode dn, Path parent) {
373 this.state = state;
374 this.dn = dn;
375 this.conflictId = state.conflictIds.get(dn);
376 this.parent = parent;
377 this.depth = parent != null ? parent.depth + 1 : 0;
378 this.children = new ArrayList<>();
379 pull(0);
380
381 this.state
382 .partitions
383 .computeIfAbsent(this.conflictId, k -> new ArrayList<>())
384 .add(this);
385 }
386
387 /**
388 * Pulls (possibly updated) scope and optional values from associated {@link DependencyNode} to this instance,
389 * going down toward children recursively the required count of levels.
390 */
391 private void pull(int levels) {
392 Dependency d = dn.getDependency();
393 if (d != null) {
394 this.scope = d.getScope();
395 this.optional = d.isOptional();
396 } else {
397 this.scope = "";
398 this.optional = false;
399 }
400 int newLevels = levels - 1;
401 if (newLevels >= 0) {
402 for (Path child : this.children) {
403 child.pull(newLevels);
404 }
405 }
406 }
407
408 /**
409 * Derives (from this to children direction) values that are "inherited" in tree: scope and optionality in the tree
410 * recursively going down required count of "levels".
411 */
412 private void derive(int levels, boolean winner) throws RepositoryException {
413 if (!winner) {
414 if (this.parent != null) {
415 if ((dn.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) {
416 ScopeContext context = new ScopeContext(this.parent.scope, this.scope);
417 state.scopeDeriver.deriveScope(context);
418 this.scope = context.derivedScope;
419 }
420 if ((dn.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) {
421 if (!this.optional && this.parent.optional) {
422 this.optional = true;
423 }
424 }
425 } else {
426 this.scope = "";
427 this.optional = false;
428 }
429 }
430 int newLevels = levels - 1;
431 if (newLevels >= 0) {
432 for (Path child : children) {
433 child.derive(newLevels, false);
434 }
435 }
436 }
437
438 /**
439 * Pushes (applies) the scope and optional and structural changes to associated {@link DependencyNode} modifying
440 * the graph of it. Verbosity is observed, and depending on it the conflicting/loser nodes are removed, or
441 * just their children is removed (with special care for version ranges, see {@link #relatedSiblingsCount(Artifact, Path)}
442 * or by just doing nothing with them only marking losers in full verbosity mode.
443 */
444 private void push(int levels) {
445 if (this.parent != null) {
446 Path winner = this.state.resolvedIds.get(this.conflictId);
447 if (winner == null) {
448 throw new IllegalStateException(
449 "Winner selection did not happen for conflictId=" + this.conflictId);
450 }
451 if (!Objects.equals(winner.conflictId, this.conflictId)) {
452 throw new IllegalStateException(
453 "ConflictId mix-up: this=" + this.conflictId + " winner=" + winner.conflictId);
454 }
455
456 if (winner == this) {
457 // copy onto dn; if applicable
458 if (this.dn.getDependency() != null) {
459 this.dn.setData(
460 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
461 this.dn.getDependency().getScope());
462 this.dn.setData(
463 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
464 this.dn.getDependency().getOptional());
465 this.dn.setScope(this.scope);
466 this.dn.setOptional(this.optional);
467 }
468 } else {
469 // loser; move out of scope
470 moveOutOfScope();
471 boolean markLoser = false;
472 switch (state.verbosity) {
473 case NONE:
474 // remove this dn
475 this.parent.children.remove(this);
476 this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren()));
477 this.parent.dn.getChildren().remove(this.dn);
478 this.children.clear();
479 break;
480 case STANDARD:
481 // leave this dn; remove children
482 String artifactId = ArtifactIdUtils.toId(this.dn.getArtifact());
483 String winnerArtifactId = ArtifactIdUtils.toId(winner.dn.getArtifact());
484 if (!Objects.equals(artifactId, winnerArtifactId)
485 && relatedSiblingsCount(this.dn.getArtifact(), this.parent) > 1) {
486 this.parent.children.remove(this);
487 this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren()));
488 this.parent.dn.getChildren().remove(this.dn);
489 this.children.clear();
490 } else {
491 this.children.clear();
492 this.dn.setChildren(Collections.emptyList());
493 markLoser = true;
494 }
495 break;
496 case FULL:
497 // leave all in place (even cycles)
498 markLoser = true;
499 break;
500 default:
501 throw new IllegalArgumentException("Unknown " + state.verbosity);
502 }
503 if (markLoser) {
504 // copy dn
505 DependencyNode dnCopy = new DefaultDependencyNode(this.dn);
506 dnCopy.setData(ConflictResolver.NODE_DATA_WINNER, winner.dn);
507 dnCopy.setData(
508 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
509 this.dn.getDependency().getScope());
510 dnCopy.setData(
511 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
512 this.dn.getDependency().getOptional());
513 dnCopy.setScope(this.scope);
514 dnCopy.setOptional(this.optional);
515 if (ConflictResolver.Verbosity.FULL != state.verbosity) {
516 dnCopy.getChildren().clear();
517 }
518
519 // swap it out in DN graph
520 this.parent
521 .dn
522 .getChildren()
523 .set(this.parent.dn.getChildren().indexOf(this.dn), dnCopy);
524 this.dn = dnCopy;
525 }
526 }
527 }
528 if (!this.children.isEmpty()) {
529 int newLevels = levels - 1;
530 if (newLevels >= 0) {
531 // child may remove itself from iterated list
532 for (Path child : new ArrayList<>(children)) {
533 child.push(newLevels);
534 }
535 }
536 } else if (!this.dn.getChildren().isEmpty()) {
537 this.dn.setChildren(Collections.emptyList());
538 }
539 }
540
541 /**
542 * Counts "relatives" (GACE equal) artifacts under same parent; this is for cleaning up redundant nodes in
543 * case of version ranges, where same GACE is resolved into multiple GACEV as range is resolved. In {@link ConflictResolver.Verbosity#STANDARD}
544 * verbosity mode we remove "redundant" nodes (of a range) leaving only "winner equal" loser, that have same GACEV as winner.
545 */
546 private int relatedSiblingsCount(Artifact artifact, Path parent) {
547 String ga = artifact.getGroupId() + ":" + artifact.getArtifactId();
548 return Math.toIntExact(parent.children.stream()
549 .map(n -> n.dn.getArtifact())
550 .filter(a -> ga.equals(a.getGroupId() + ":" + a.getArtifactId()))
551 .count());
552 }
553
554 /**
555 * Removes this and all child {@link Path} nodes from winner selection scope; essentially marks whole subtree
556 * from "this and below" as loser, to not be considered in subsequent winner selections.
557 */
558 private void moveOutOfScope() {
559 this.state.partitions.get(this.conflictId).remove(this);
560 for (Path child : this.children) {
561 child.moveOutOfScope();
562 }
563 }
564
565 /**
566 * Adds node children: this method should be "batch" used, as all (potential) children should be added at once.
567 * Method will return really added ones, as this class avoids cycles.
568 */
569 private List<Path> addChildren(List<DependencyNode> children) throws RepositoryException {
570 Collection<String> thisCyclicPredecessors =
571 this.state.cyclicPredecessors.getOrDefault(this.conflictId, Collections.emptySet());
572
573 ArrayList<Path> added = new ArrayList<>(children.size());
574 for (DependencyNode child : children) {
575 String childConflictId = this.state.conflictIds.get(child);
576 if (!this.state.partitions.containsKey(childConflictId)
577 || !thisCyclicPredecessors.contains(childConflictId)) {
578 Path c = new Path(this.state, child, this);
579 this.children.add(c);
580 c.derive(0, false);
581 added.add(c);
582 }
583 }
584 return added;
585 }
586
587 /**
588 * Dump for debug.
589 */
590 private void dump(String padding) {
591 System.out.println(padding + this.dn + ": " + this.scope + "/" + this.optional);
592 for (Path child : children) {
593 child.dump(padding + " ");
594 }
595 }
596
597 /**
598 * For easier debug.
599 */
600 @Override
601 public String toString() {
602 return this.dn.toString();
603 }
604 }
605
606 /**
607 * A context used to hold information that is relevant for deriving the scope of a child dependency.
608 *
609 * @see ConflictResolver.ScopeDeriver
610 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
611 * change without notice and only exists to enable unit testing.
612 */
613 private static final class ScopeContext extends ConflictResolver.ScopeContext {
614 private final String parentScope;
615 private final String childScope;
616 private String derivedScope;
617
618 /**
619 * Creates a new scope context with the specified properties.
620 *
621 * @param parentScope The scope of the parent dependency, may be {@code null}.
622 * @param childScope The scope of the child dependency, may be {@code null}.
623 * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
624 * change without notice and only exists to enable unit testing.
625 */
626 private ScopeContext(String parentScope, String childScope) {
627 this.parentScope = (parentScope != null) ? parentScope : "";
628 this.derivedScope = (childScope != null) ? childScope : "";
629 this.childScope = (childScope != null) ? childScope : "";
630 }
631
632 /**
633 * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
634 * the scope deriver.
635 *
636 * @return The scope of the parent dependency, never {@code null}.
637 */
638 public String getParentScope() {
639 return parentScope;
640 }
641
642 /**
643 * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
644 * descriptor of the parent dependency.
645 *
646 * @return The original scope of the child dependency, never {@code null}.
647 */
648 public String getChildScope() {
649 return childScope;
650 }
651
652 /**
653 * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
654 * scope deriver makes changes.
655 *
656 * @return The derived scope of the child dependency, never {@code null}.
657 */
658 public String getDerivedScope() {
659 return derivedScope;
660 }
661
662 /**
663 * Sets the derived scope of the child dependency.
664 *
665 * @param derivedScope The derived scope of the dependency, may be {@code null}.
666 */
667 public void setDerivedScope(String derivedScope) {
668 this.derivedScope = (derivedScope != null) ? derivedScope : "";
669 }
670 }
671
672 /**
673 * A conflicting dependency.
674 *
675 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
676 * change without notice and only exists to enable unit testing.
677 */
678 private static final class ConflictItem extends ConflictResolver.ConflictItem {
679 private final Path path;
680 private final List<DependencyNode> parent;
681 private final Artifact artifact;
682 private final DependencyNode node;
683 private final int depth;
684 private final String scope;
685 private final int optionalities;
686
687 /**
688 * Bit flag indicating whether one or more paths consider the dependency non-optional.
689 */
690 public static final int OPTIONAL_FALSE = 0x01;
691
692 /**
693 * Bit flag indicating whether one or more paths consider the dependency optional.
694 */
695 public static final int OPTIONAL_TRUE = 0x02;
696
697 private ConflictItem(Path path) {
698 this.path = path;
699 if (path.parent != null) {
700 DependencyNode parent = path.parent.dn;
701 this.parent = parent.getChildren();
702 this.artifact = parent.getArtifact();
703 } else {
704 this.parent = null;
705 this.artifact = null;
706 }
707 this.node = path.dn;
708 this.depth = path.depth;
709 this.scope = path.scope;
710 this.optionalities = path.optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
711 }
712
713 /**
714 * Determines whether the specified conflict item is a sibling of this item.
715 *
716 * @param item The other conflict item, must not be {@code null}.
717 * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise.
718 */
719 @Override
720 public boolean isSibling(ConflictResolver.ConflictItem item) {
721 return parent == ((ConflictItem) item).parent;
722 }
723
724 /**
725 * Gets the dependency node involved in the conflict.
726 *
727 * @return The involved dependency node, never {@code null}.
728 */
729 @Override
730 public DependencyNode getNode() {
731 return node;
732 }
733
734 /**
735 * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
736 *
737 * @return The involved dependency, never {@code null}.
738 */
739 @Override
740 public Dependency getDependency() {
741 return node.getDependency();
742 }
743
744 /**
745 * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
746 * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
747 * possible depth.
748 *
749 * @return The zero-based depth of the node in the graph.
750 */
751 @Override
752 public int getDepth() {
753 return depth;
754 }
755
756 /**
757 * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
758 * different paths and each path might result in a different derived scope.
759 *
760 * @see ConflictResolver.ScopeDeriver
761 * @return The (read-only) set of derived scopes of the dependency, never {@code null}.
762 */
763 @Override
764 public Collection<String> getScopes() {
765 return Collections.singleton(scope);
766 }
767
768 /**
769 * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
770 * different paths and each path might result in a different derived optionality.
771 *
772 * @return A bit field consisting of {@link PathConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
773 * {@link PathConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
774 * dependency was encountered with.
775 */
776 @Override
777 public int getOptionalities() {
778 return optionalities;
779 }
780
781 @Override
782 public String toString() {
783 return node + " @ " + depth + " < " + artifact;
784 }
785 }
786
787 /**
788 * A context used to hold information that is relevant for resolving version and scope conflicts.
789 *
790 * @see ConflictResolver.VersionSelector
791 * @see ConflictResolver.ScopeSelector
792 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
793 * change without notice and only exists to enable unit testing.
794 */
795 private static final class ConflictContext extends ConflictResolver.ConflictContext {
796 private final DependencyNode root;
797 private final Map<DependencyNode, String> conflictIds;
798 private final Collection<ConflictResolver.ConflictItem> items;
799 private final String conflictId;
800
801 // elected properties
802 private ConflictItem winner;
803 private String scope;
804 private Boolean optional;
805
806 private ConflictContext(
807 DependencyNode root,
808 Map<DependencyNode, String> conflictIds,
809 Collection<ConflictItem> items,
810 String conflictId) {
811 this.root = root;
812 this.conflictIds = conflictIds;
813 this.items = Collections.unmodifiableCollection(items);
814 this.conflictId = conflictId;
815 }
816
817 /**
818 * Gets the root node of the dependency graph being transformed.
819 *
820 * @return The root node of the dependency graph, never {@code null}.
821 */
822 @Override
823 public DependencyNode getRoot() {
824 return root;
825 }
826
827 /**
828 * Determines whether the specified dependency node belongs to this conflict context.
829 *
830 * @param node The dependency node to check, must not be {@code null}.
831 * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise.
832 */
833 @Override
834 public boolean isIncluded(DependencyNode node) {
835 return conflictId.equals(conflictIds.get(node));
836 }
837
838 /**
839 * Gets the collection of conflict items in this context.
840 *
841 * @return The (read-only) collection of conflict items in this context, never {@code null}.
842 */
843 @Override
844 public Collection<ConflictResolver.ConflictItem> getItems() {
845 return items;
846 }
847
848 /**
849 * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
850 *
851 * @return The winning conflict item or {@code null} if not set yet.
852 */
853 @Override
854 public ConflictResolver.ConflictItem getWinner() {
855 return winner;
856 }
857
858 /**
859 * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
860 *
861 * @param winner The winning conflict item, may be {@code null}.
862 */
863 @Override
864 public void setWinner(ConflictResolver.ConflictItem winner) {
865 this.winner = (ConflictItem) winner;
866 }
867
868 /**
869 * Gets the effective scope of the winning dependency.
870 *
871 * @return The effective scope of the winning dependency or {@code null} if none.
872 */
873 @Override
874 public String getScope() {
875 return scope;
876 }
877
878 /**
879 * Sets the effective scope of the winning dependency.
880 *
881 * @param scope The effective scope, may be {@code null}.
882 */
883 @Override
884 public void setScope(String scope) {
885 this.scope = scope;
886 }
887
888 /**
889 * Gets the effective optional flag of the winning dependency.
890 *
891 * @return The effective optional flag or {@code null} if none.
892 */
893 @Override
894 public Boolean getOptional() {
895 return optional;
896 }
897
898 /**
899 * Sets the effective optional flag of the winning dependency.
900 *
901 * @param optional The effective optional flag, may be {@code null}.
902 */
903 @Override
904 public void setOptional(Boolean optional) {
905 this.optional = optional;
906 }
907
908 @Override
909 public String toString() {
910 return winner + " @ " + scope + " < " + items;
911 }
912 }
913 }