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.IdentityHashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29
30 import org.eclipse.aether.ConfigurationProperties;
31 import org.eclipse.aether.RepositoryException;
32 import org.eclipse.aether.RepositorySystemSession;
33 import org.eclipse.aether.artifact.Artifact;
34 import org.eclipse.aether.collection.DependencyGraphTransformationContext;
35 import org.eclipse.aether.graph.DefaultDependencyNode;
36 import org.eclipse.aether.graph.Dependency;
37 import org.eclipse.aether.graph.DependencyNode;
38 import org.eclipse.aether.util.ConfigUtils;
39 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
40
41 import static java.util.Objects.requireNonNull;
42
43 /**
44 * A dependency graph transformer that resolves version and scope conflicts among dependencies.
45 * This resolver builds a cycle-free parallel path tree from the dependency graph, then processes
46 * conflict groups in topologically sorted order to select winners.
47 * <p>
48 * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
49 * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
50 * be left in place with conflict information. The exact rules by which a winning node and its effective scope
51 * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
52 * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
53 * <p>
54 * <strong>Algorithm Overview:</strong>
55 * <ol>
56 * <li><strong>Path Tree Construction:</strong> Builds a cycle-free parallel tree structure from the input
57 * dependency graph, where each {@code Path} represents a unique route to a dependency node.
58 * To avoid exponential memory use in highly connected graphs, subtree expansion of each
59 * {@link DependencyNode} instance is bounded: when the same node is reached again via a
60 * different parent path, a {@code Path} entry is still created for it (so all occurrences
61 * appear in the conflict partition), but its subtree is only re-traversed if reached at a
62 * strictly shallower depth than before.</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>Memory Strategy:</strong> Uses parallel tree structure vs in-place graph modification</li>
72 * <li><strong>Cycle Handling:</strong> Explicitly breaks cycles during tree construction</li>
73 * <li><strong>Processing Order:</strong> Level-by-level from root vs depth-first traversal</li>
74 * </ul>
75 * <p>
76 * <strong>Implementation Note:</strong> This conflict resolver builds a cycle-free "parallel" structure based on the
77 * passed-in dependency graph, and applies operations level by level starting from the root. The parallel {@code Path}
78 * tree ensures that cycles in the original graph don't affect the conflict resolution algorithm's performance.
79 *
80 * @see ClassicConflictResolver
81 * @since 2.0.11
82 */
83 public final class PathConflictResolver extends ConflictResolver {
84 /**
85 * This implementation of conflict resolver is able to show more precise information regarding cycles in standard
86 * verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it
87 * may want to enable this for easier cycle detection, but in that case this conflict resolver will provide "extra nodes"
88 * not present on "standard verbosity level" with "classic" conflict resolver, that may lead to IT issues down the
89 * stream. Hence, the default is to provide as much information as much verbose "classic" does.
90 *
91 * @since 2.0.12
92 * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
93 * @configurationType {@link java.lang.Boolean}
94 * @configurationDefaultValue {@link #DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY}
95 */
96 public static final String CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY = ConfigurationProperties.PREFIX_AETHER
97 + "conflictResolver." + ConflictResolver.PATH_CONFLICT_RESOLVER + ".showCyclesInStandardVerbosity";
98
99 public static final boolean DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY = false;
100
101 private final ConflictResolver.VersionSelector versionSelector;
102 private final ConflictResolver.ScopeSelector scopeSelector;
103 private final ConflictResolver.ScopeDeriver scopeDeriver;
104 private final ConflictResolver.OptionalitySelector optionalitySelector;
105
106 /**
107 * Creates a new conflict resolver instance with the specified hooks.
108 *
109 * @param versionSelector the version selector to use, must not be {@code null}
110 * @param scopeSelector the scope selector to use, must not be {@code null}
111 * @param optionalitySelector the optionality selector ot use, must not be {@code null}
112 * @param scopeDeriver the scope deriver to use, must not be {@code null}
113 */
114 public PathConflictResolver(
115 ConflictResolver.VersionSelector versionSelector,
116 ConflictResolver.ScopeSelector scopeSelector,
117 ConflictResolver.OptionalitySelector optionalitySelector,
118 ConflictResolver.ScopeDeriver scopeDeriver) {
119 this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null");
120 this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null");
121 this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null");
122 this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null");
123 }
124
125 @SuppressWarnings("unchecked")
126 @Override
127 public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
128 throws RepositoryException {
129 requireNonNull(node, "node cannot be null");
130 requireNonNull(context, "context cannot be null");
131 List<String> sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
132 if (sortedConflictIds == null) {
133 ConflictIdSorter sorter = new ConflictIdSorter();
134 sorter.transformGraph(node, context);
135
136 sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
137 }
138
139 @SuppressWarnings("unchecked")
140 Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
141 long time1 = System.nanoTime();
142
143 Map<DependencyNode, String> conflictIds =
144 (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
145 if (conflictIds == null) {
146 throw new RepositoryException("conflict groups have not been identified");
147 }
148
149 State state = new State(
150 ConflictResolver.getVerbosity(context.getSession()),
151 ConfigUtils.getBoolean(
152 context.getSession(),
153 DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY,
154 CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY),
155 versionSelector.getInstance(node, context),
156 scopeSelector.getInstance(node, context),
157 scopeDeriver.getInstance(node, context),
158 optionalitySelector.getInstance(node, context),
159 conflictIds,
160 sortedConflictIds.size(),
161 node);
162
163 // loop over topographically sorted conflictIds
164 int conflictItemCount = 0;
165 for (String conflictId : sortedConflictIds) {
166 // paths in given conflict group to consider; filter out those moved out of scope
167 List<Path> allPaths = state.partitions.get(conflictId);
168 List<Path> activePaths = new ArrayList<>(allPaths.size());
169 List<ConflictItem> items = new ArrayList<>(allPaths.size());
170 for (Path p : allPaths) {
171 if (!p.outOfScope) {
172 activePaths.add(p);
173 items.add(new ConflictItem(p));
174 }
175 }
176 // Replace partition entry with filtered list to release references to out-of-scope
177 // paths (and their detached subtrees), allowing GC during resolution
178 state.partitions.put(conflictId, activePaths);
179 if (activePaths.isEmpty()) {
180 // this means that whole group "fall out of scope" (are all on loser branches); skip
181 continue;
182 }
183 conflictItemCount += activePaths.size();
184
185 // create conflict context for given conflictId
186 ConflictContext ctx = new ConflictContext(node, state.conflictIds, items, conflictId);
187
188 // select winner (is done by VersionSelector)
189 state.versionSelector.selectVersion(ctx);
190 if (ctx.winner == null) {
191 throw new RepositoryException("conflict resolver did not select winner among " + items);
192 }
193 // select scope (no side effect between this and above operations)
194 state.scopeSelector.selectScope(ctx);
195 // select optionality (no side effect between this and above operations)
196 state.optionalitySelector.selectOptionality(ctx);
197
198 // we have a winner path
199 Path winnerPath = ctx.winner.path;
200
201 // mark conflictId as resolved with winner; sanity check
202 if (state.resolvedIds.containsKey(conflictId)) {
203 throw new RepositoryException("conflict resolver already have winner for conflictId=" + conflictId
204 + ": " + state.resolvedIds);
205 }
206 state.resolvedIds.put(conflictId, winnerPath);
207
208 // loop over considered paths and apply selection results
209 for (Path path : activePaths) {
210 // apply selected properties scope/optional to winner (winner carries version; others are losers)
211 if (path == winnerPath) {
212 path.scope = ctx.scope;
213 path.optional = ctx.optional;
214 }
215
216 // reset children as inheritance may be affected by this node scope/optionality change
217 if (path.children != null) {
218 for (Path c : path.children) {
219 c.pull(0);
220 }
221 }
222 // derive with new values from this to children only; observe winner flag
223 path.derive(1, path == winnerPath);
224 // push this node full level changes to DN graph
225 path.push(0);
226 }
227 }
228
229 if (stats != null) {
230 long time2 = System.nanoTime();
231 stats.put("ConflictResolver.totalTime", time2 - time1);
232 stats.put("ConflictResolver.conflictItemCount", conflictItemCount);
233 }
234
235 return node;
236 }
237
238 /**
239 * State of conflict resolution processing, to make this component (held in session) re-entrant by multiple threads.
240 */
241 private static class State {
242 /**
243 * Verbosity to be applied, see {@link ConflictResolver.Verbosity}.
244 */
245 private final ConflictResolver.Verbosity verbosity;
246
247 /**
248 * Whether to show nodes entering cycles, for easier identification. If this is enabled, this implementation
249 * of conflict resolver will show more data than classic.
250 */
251 private final boolean showCyclesInStandardVerbosity;
252
253 /**
254 * The {@link ConflictResolver.VersionSelector} to use.
255 */
256 private final ConflictResolver.VersionSelector versionSelector;
257
258 /**
259 * The {@link ConflictResolver.ScopeSelector} to use.
260 */
261 private final ConflictResolver.ScopeSelector scopeSelector;
262
263 /**
264 * The {@link ConflictResolver.ScopeDeriver} to use.
265 */
266 private final ConflictResolver.ScopeDeriver scopeDeriver;
267
268 /**
269 * The {@link ConflictResolver.OptionalitySelector} to use/
270 */
271 private final ConflictResolver.OptionalitySelector optionalitySelector;
272
273 /**
274 * The node to conflictId mapping from {@link ConflictMarker}.
275 */
276 private final Map<DependencyNode, String> conflictIds;
277
278 /**
279 * A mapping from conflictId to paths represented as {@link Path}s that exist for each conflictId. In other
280 * words all paths to each {@link DependencyNode} that are member of same conflictId group.
281 * Uses {@link ArrayList} per partition; out-of-scope paths are marked via {@link Path#outOfScope} flag
282 * and filtered at query time, avoiding the per-entry overhead of LinkedHashSet/HashMap.Node.
283 */
284 private final Map<String, List<Path>> partitions;
285
286 /**
287 * A mapping from conflictIds to winner {@link Path}, hence {@link DependencyNode} for given conflictId.
288 */
289 private final Map<String, Path> resolvedIds;
290
291 /**
292 * The root {@link Path}.
293 */
294 private final Path root;
295
296 /**
297 * Pooled {@link ScopeContext} instance reused across derive() calls to avoid allocating a new
298 * object per node. Reset via {@link ScopeContext#reset(String, String)} before each use.
299 */
300 private final ScopeContext scopeContext;
301
302 /**
303 * Tracks the minimum depth at which each {@link DependencyNode} instance has been expanded
304 * (i.e. its children visited) during {@link #gatherCRNodes(Path)}. Used to bound subtree
305 * re-traversal in highly connected graphs.
306 * <p>
307 * When the same {@link DependencyNode} is reached again via a different parent path, a
308 * {@link Path} entry is always created for it (so all occurrences appear in the conflict
309 * partition for winner selection). The subtree is only re-expanded if the new occurrence
310 * is at a strictly shallower depth than the recorded minimum — ensuring that the shallowest
311 * reachable occurrence drives expansion, while deeper duplicates are skipped.
312 * <p>
313 * Without this guard, a highly connected graph where node X is reachable via N different
314 * parents causes X's subtree to be expanded N times, leading to exponential {@link Path}
315 * creation and {@link OutOfMemoryError} on large multi-module reactors.
316 * <p>
317 * Uses {@link IdentityHashMap} because {@link DependencyNode} instances are shared objects
318 * in the dependency graph — identity equality is both correct and faster than equals/hashCode.
319 */
320 private final Map<DependencyNode, Integer> expandedNodes;
321
322 @SuppressWarnings("checkstyle:ParameterNumber")
323 private State(
324 ConflictResolver.Verbosity verbosity,
325 boolean showCyclesInStandardVerbosity,
326 ConflictResolver.VersionSelector versionSelector,
327 ConflictResolver.ScopeSelector scopeSelector,
328 ConflictResolver.ScopeDeriver scopeDeriver,
329 ConflictResolver.OptionalitySelector optionalitySelector,
330 Map<DependencyNode, String> conflictIds,
331 int conflictIdCount,
332 DependencyNode node)
333 throws RepositoryException {
334 this.verbosity = verbosity;
335 this.showCyclesInStandardVerbosity = showCyclesInStandardVerbosity;
336 this.versionSelector = versionSelector;
337 this.scopeSelector = scopeSelector;
338 this.scopeDeriver = scopeDeriver;
339 this.optionalitySelector = optionalitySelector;
340 this.conflictIds = conflictIds;
341 // Right-size maps: conflictIdCount gives exact number of partitions and resolved entries
342 this.partitions = new HashMap<>(conflictIdCount * 4 / 3 + 1);
343 this.resolvedIds = new HashMap<>(conflictIdCount * 4 / 3 + 1);
344 this.scopeContext = new ScopeContext(null, null);
345 this.expandedNodes = new IdentityHashMap<>();
346 this.root = build(node);
347 }
348
349 /**
350 * Consumes the dirty graph and builds internal structures out of {@link Path} instances that is always a
351 * tree. As a side effect, {@link #partitions} are being filled up as well, that combined with topo
352 * sorted conflictIds can serve as a starting to point to walk the graph.
353 */
354 private Path build(DependencyNode node) throws RepositoryException {
355 String nodeConflictId = this.conflictIds.get(node);
356 Path root = new Path(this, node, nodeConflictId, null);
357 gatherCRNodes(root);
358 return root;
359 }
360
361 /**
362 * Iteratively builds {@link Path} graph by observing each node associated {@link DependencyNode}.
363 * Uses an explicit stack instead of recursion to avoid {@link StackOverflowError} on very deep
364 * dependency graphs (reported in large multi-module projects with 13+ levels of recursion).
365 * <p>
366 * Subtree expansion of each {@link DependencyNode} instance is bounded: when the same node is
367 * reached again via a different parent path, a {@link Path} entry is still created for it (so
368 * all occurrences appear in the conflict partition for winner selection), but its subtree is only
369 * re-traversed if reached at a strictly shallower depth than before. This prevents exponential
370 * {@link Path} creation in highly connected graphs (e.g. a 813-module reactor where each module
371 * depends on ~9 others).
372 */
373 private void gatherCRNodes(Path root) throws RepositoryException {
374 ArrayList<Path> stack = new ArrayList<>();
375 stack.add(root);
376 while (!stack.isEmpty()) {
377 Path node = stack.remove(stack.size() - 1);
378 List<DependencyNode> children = node.dn.getChildren();
379 if (!children.isEmpty()) {
380 // add children; we will get back those really added (not causing cycles)
381 List<Path> added = node.addChildren(children);
382 // push in reverse order so first child is processed first (DFS order),
383 // but only if this DependencyNode instance hasn't been expanded at a shallower depth
384 for (int i = added.size() - 1; i >= 0; i--) {
385 Path child = added.get(i);
386 Integer prevDepth = expandedNodes.get(child.dn);
387 if (prevDepth == null || child.depth < prevDepth) {
388 expandedNodes.put(child.dn, child.depth);
389 stack.add(child);
390 }
391 // else: child.dn was already expanded at an equal or shallower depth;
392 // the Path is already in the partition (created by addChildren), but we
393 // skip re-expanding its subtree since the existing expansion already covered
394 // all reachable descendants.
395 }
396 }
397 }
398 }
399 }
400
401 /**
402 * Represents a unique path within the dependency graph from the root to a specific {@link DependencyNode}.
403 * This is the core data structure that enables the O(N) performance of {@link PathConflictResolver}.
404 * <p>
405 * <strong>Key Concepts:</strong>
406 * <ul>
407 * <li><strong>Path Uniqueness:</strong> Each {@code Path} instance represents a distinct route through
408 * the dependency graph, even if multiple paths lead to the same {@code DependencyNode}</li>
409 * <li><strong>Cycle-Free Structure:</strong> The {@code Path} tree is guaranteed to be acyclic, even
410 * when the original dependency graph contains cycles</li>
411 * <li><strong>Parallel Structure:</strong> This creates a "clean" tree alongside the original "dirty"
412 * graph for efficient processing</li>
413 * </ul>
414 * <p>
415 * <strong>Example:</strong> If dependency A appears in the graph via two different routes:
416 * <pre>
417 * Root → B → A (path 1)
418 * Root → C → A (path 2)
419 * </pre>
420 * Two separate {@code Path} instances will be created, both pointing to the same {@code DependencyNode} A,
421 * but representing different paths through the dependency tree.
422 * <p>
423 * <strong>Memory Optimization:</strong> While this creates additional objects, it enables the algorithm
424 * to process conflicts in O(N) time rather than O(N²), making it much more efficient for large graphs.
425 * <p>
426 * <strong>Conflict Resolution:</strong> Paths are grouped by conflict ID (based on groupId:artifactId:classifier:extension coordinates),
427 * and the conflict resolution algorithm can efficiently process each group independently.
428 */
429 private static class Path {
430 // given
431 private final State state;
432 private DependencyNode dn;
433 private final String conflictId;
434 private final Path parent;
435 // derived
436 private final int depth;
437 // Lazy: null for leaf nodes (never populated by addChildren), right-sized for non-leaves.
438 // This avoids allocating an ArrayList + backing array for every leaf node in the tree
439 // (typically 60-70% of all nodes), saving ~40 bytes per leaf.
440 private List<Path> children;
441 // mutated
442 private String scope;
443 private boolean optional;
444 // Flag used instead of removing from partition sets; avoids LinkedHashSet overhead (~48 bytes/entry)
445 private boolean outOfScope;
446
447 private Path(State state, DependencyNode dn, String conflictId, Path parent) {
448 this.state = state;
449 this.dn = dn;
450 this.conflictId = conflictId;
451 this.parent = parent;
452 this.depth = parent != null ? parent.depth + 1 : 0;
453 pull(0);
454
455 this.state
456 .partitions
457 .computeIfAbsent(this.conflictId, k -> new ArrayList<>())
458 .add(this);
459 }
460
461 /**
462 * Checks whether the given conflictId appears on the path from this node to the root.
463 * Walks the parent chain comparing conflict IDs. Since dependency tree depth is bounded
464 * in practice (< 30), each check is fast while avoiding per-node {@link java.util.HashSet}
465 * allocation that was a major JFR hotspot (~45% CPU) in large multi-module builds.
466 */
467 private boolean hasConflictIdOnPathToRoot(String targetConflictId) {
468 for (Path current = this; current != null; current = current.parent) {
469 if (targetConflictId.equals(current.conflictId)) {
470 return true;
471 }
472 }
473 return false;
474 }
475
476 /**
477 * Pulls (possibly updated) scope and optional values from associated {@link DependencyNode} to this instance,
478 * going down toward children recursively the required count of levels.
479 */
480 private void pull(int levels) {
481 Dependency d = dn.getDependency();
482 if (d != null) {
483 this.scope = d.getScope();
484 this.optional = d.isOptional();
485 } else {
486 this.scope = "";
487 this.optional = false;
488 }
489 int newLevels = levels - 1;
490 if (newLevels >= 0 && this.children != null) {
491 for (Path child : this.children) {
492 child.pull(newLevels);
493 }
494 }
495 }
496
497 /**
498 * Derives (from this to children direction) values that are "inherited" in tree: scope and optionality in the tree
499 * recursively going down required count of "levels".
500 */
501 private void derive(int levels, boolean winner) throws RepositoryException {
502 if (!winner) {
503 if (this.parent != null) {
504 if ((dn.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) {
505 state.scopeContext.reset(this.parent.scope, this.scope);
506 state.scopeDeriver.deriveScope(state.scopeContext);
507 this.scope = state.scopeContext.derivedScope;
508 }
509 if ((dn.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) {
510 if (!this.optional && this.parent.optional) {
511 this.optional = true;
512 }
513 }
514 } else {
515 this.scope = "";
516 this.optional = false;
517 }
518 }
519 int newLevels = levels - 1;
520 if (newLevels >= 0 && this.children != null) {
521 for (Path child : this.children) {
522 child.derive(newLevels, false);
523 }
524 }
525 }
526
527 /**
528 * Pushes (applies) the scope and optional and structural changes to associated {@link DependencyNode} modifying
529 * the graph of it. Verbosity is observed, and depending on it the conflicting/loser nodes are removed, or
530 * just their children is removed (with special care for version ranges, see {@link #relatedSiblingsCount(Artifact, Path)}
531 * or by just doing nothing with them only marking losers in full verbosity mode.
532 */
533 private void push(int levels) {
534 if (this.parent != null) {
535 Path winner = this.state.resolvedIds.get(this.conflictId);
536 if (winner == null) {
537 throw new IllegalStateException(
538 "Winner selection did not happen for conflictId=" + this.conflictId);
539 }
540 if (!Objects.equals(winner.conflictId, this.conflictId)) {
541 throw new IllegalStateException(
542 "ConflictId mix-up: this=" + this.conflictId + " winner=" + winner.conflictId);
543 }
544
545 if (winner == this) {
546 // copy onto dn; if applicable
547 if (this.dn.getDependency() != null) {
548 this.dn.setData(
549 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
550 this.dn.getDependency().getScope());
551 this.dn.setData(
552 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
553 this.dn.getDependency().getOptional());
554 this.dn.setScope(this.scope);
555 this.dn.setOptional(this.optional);
556 }
557 } else {
558 // loser; move out of scope
559 moveOutOfScope();
560 boolean markLoser = false;
561 switch (state.verbosity) {
562 case NONE:
563 // remove loser dn
564 if (this.parent.children != null) {
565 this.parent.children.remove(this);
566 }
567 this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren()));
568 this.parent.dn.getChildren().remove(this.dn);
569 this.children = null;
570 break;
571 case STANDARD:
572 // is redundant if:
573 // - is not same as winner, and has related siblings (version range)
574 // - same instance of DN is direct dependency on path leading here
575 boolean isRedundant =
576 (!ArtifactIdUtils.equalsId(this.dn.getArtifact(), winner.dn.getArtifact())
577 && relatedSiblingsCount(this.dn.getArtifact(), this.parent) > 1);
578 if (!this.state.showCyclesInStandardVerbosity) {
579 isRedundant = isRedundant
580 || this.parent.isDirectDependencyOnPathToRoot(this.dn.getArtifact());
581 }
582 if (isRedundant) {
583 // is redundant dn; remove dn
584 if (this.parent.children != null) {
585 this.parent.children.remove(this);
586 }
587 this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren()));
588 this.parent.dn.getChildren().remove(this.dn);
589 this.children = null;
590 } else {
591 // copy loser dn; without children
592 DependencyNode dnCopy = new DefaultDependencyNode(this.dn);
593 dnCopy.setChildren(Collections.emptyList());
594
595 // swap it out in DN graph; in case of cycles this may happen more than once
596 int idx = this.parent.dn.getChildren().indexOf(this.dn);
597 if (idx >= 0) {
598 this.parent.dn.getChildren().set(idx, dnCopy);
599 }
600 this.dn = dnCopy;
601
602 this.children = null;
603 markLoser = true;
604 }
605 break;
606 case FULL:
607 // copy loser dn; with children
608 DependencyNode dnCopy = new DefaultDependencyNode(this.dn);
609 dnCopy.setChildren(new ArrayList<>(this.dn.getChildren()));
610
611 // swap it out in DN graph; in case of cycles this may happen more than once
612 int idx = this.parent.dn.getChildren().indexOf(this.dn);
613 if (idx >= 0) {
614 this.parent.dn.getChildren().set(idx, dnCopy);
615 }
616 this.dn = dnCopy;
617
618 markLoser = true;
619 break;
620 default:
621 throw new IllegalArgumentException("Unknown " + state.verbosity);
622 }
623 if (markLoser) {
624 this.dn.setData(ConflictResolver.NODE_DATA_WINNER, winner.dn);
625 this.dn.setData(
626 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
627 this.dn.getDependency().getScope());
628 this.dn.setData(
629 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
630 this.dn.getDependency().getOptional());
631 this.dn.setScope(this.scope);
632 this.dn.setOptional(this.optional);
633 }
634 }
635 }
636
637 // Note: push() is always called with levels=0, so newLevels would be -1
638 // and the recursive block would never execute. The recursive structure is
639 // intentionally not present; all push() calls happen from the main loop.
640 }
641
642 /**
643 * Returns {@code true} if given artifact is a direct dependency on the path leading from this toward root.
644 * A "direct dependency" is one at depth 1 (immediate child of root). Rather than recursing through every
645 * ancestor, this walks directly to the depth-1 node and performs one allocation-free comparison.
646 * <p>
647 * Note: this check and use of this method is ONLY present to make this conflict resolver produce SAME output
648 * as {@link ClassicConflictResolver} does, but IMHO this rule here is very arbitrary, moreover, in "standard"
649 * (where it is only used) verbosity it in facts HIDES the trace of possible cycles.
650 *
651 * @see #CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY
652 */
653 private boolean isDirectDependencyOnPathToRoot(Artifact artifact) {
654 // Walk up to depth-1 ancestor (direct dependency of root) instead of recursing every level
655 Path current = this;
656 while (current != null && current.depth > 1) {
657 current = current.parent;
658 }
659 return current != null
660 && current.depth == 1
661 && ArtifactIdUtils.equalsVersionlessId(current.dn.getArtifact(), artifact);
662 }
663
664 /**
665 * Counts "relatives" (GACE equal) artifacts under same parent; this is for cleaning up redundant nodes in
666 * case of version ranges, where same GACE is resolved into multiple GACEV as range is resolved. In {@link ConflictResolver.Verbosity#STANDARD}
667 * verbosity mode we remove "redundant" nodes (of a range) leaving only "winner equal" loser, that have same GACEV as winner.
668 */
669 private int relatedSiblingsCount(Artifact artifact, Path parent) {
670 if (parent.children == null) {
671 return 0;
672 }
673 String groupId = artifact.getGroupId();
674 String artifactId = artifact.getArtifactId();
675 int count = 0;
676 for (Path n : parent.children) {
677 Artifact a = n.dn.getArtifact();
678 if (Objects.equals(groupId, a.getGroupId()) && Objects.equals(artifactId, a.getArtifactId())) {
679 count++;
680 }
681 }
682 return count;
683 }
684
685 /**
686 * Marks this and all child {@link Path} nodes as out of scope; essentially marks whole subtree
687 * from "this and below" as loser, to not be considered in subsequent winner selections.
688 * Uses a boolean flag instead of removing from partition collections, avoiding the per-entry
689 * overhead of LinkedHashSet (~48 bytes/entry). Out-of-scope paths are filtered at query time.
690 * <p>
691 * Uses an explicit stack instead of recursion to avoid {@link StackOverflowError} on deep
692 * dependency graphs, consistent with the iterative approach in
693 * {@link State#gatherCRNodes(Path)}. Also nulls children references on out-of-scope nodes
694 * to allow GC of detached subtrees during resolution.
695 */
696 private void moveOutOfScope() {
697 ArrayList<Path> stack = new ArrayList<>();
698 stack.add(this);
699 while (!stack.isEmpty()) {
700 Path node = stack.remove(stack.size() - 1);
701 node.outOfScope = true;
702 if (node.children != null) {
703 stack.addAll(node.children);
704 node.children = null;
705 }
706 }
707 }
708
709 /**
710 * Adds node children: this method should be "batch" used, as all (potential) children should be added at once.
711 * Method will return really added {@link Path} instances, as this class avoids cycles. Those forming a cycle
712 * are not recursed (not returned in list), keeping {@link Path} cycle free.
713 * <p>
714 * Cycle detection is performed via {@link #hasConflictIdOnPathToRoot(String)} which walks
715 * the parent chain comparing conflict IDs. Since dependency tree depth is bounded in
716 * practice (< 30), each check is fast while avoiding per-node HashSet allocation.
717 * This implies that this conflict resolver, by its nature "redoes" the
718 * {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} calculated by {@link ConflictIdSorter}.
719 */
720 private List<Path> addChildren(List<DependencyNode> children) throws RepositoryException {
721 // Right-size the children list to avoid ArrayList default capacity waste
722 this.children = new ArrayList<>(children.size());
723 ArrayList<Path> added = new ArrayList<>(children.size());
724 for (DependencyNode child : children) {
725 String childConflictId = this.state.conflictIds.get(child);
726 boolean cycle = hasConflictIdOnPathToRoot(childConflictId);
727 Path c = new Path(this.state, child, childConflictId, this);
728 this.children.add(c);
729 c.derive(0, false);
730 if (!cycle) {
731 added.add(c);
732 }
733 }
734 return added;
735 }
736
737 /**
738 * Dump for debug.
739 */
740 private void dump(String padding) {
741 System.out.println(padding + this.dn + ": " + this.scope + "/" + this.optional);
742 if (this.children != null) {
743 for (Path child : this.children) {
744 child.dump(padding + " ");
745 }
746 }
747 }
748
749 /**
750 * For easier debug.
751 */
752 @Override
753 public String toString() {
754 return this.dn.toString();
755 }
756 }
757
758 /**
759 * A context used to hold information that is relevant for deriving the scope of a child dependency.
760 *
761 * @see ConflictResolver.ScopeDeriver
762 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
763 * change without notice and only exists to enable unit testing
764 */
765 private static final class ScopeContext extends ConflictResolver.ScopeContext {
766 private String parentScope;
767 private String childScope;
768 private String derivedScope;
769
770 /**
771 * Creates a new scope context with the specified properties.
772 *
773 * @param parentScope the scope of the parent dependency, may be {@code null}
774 * @param childScope the scope of the child dependency, may be {@code null}
775 * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
776 * change without notice and only exists to enable unit testing
777 */
778 private ScopeContext(String parentScope, String childScope) {
779 this.parentScope = (parentScope != null) ? parentScope : "";
780 this.derivedScope = (childScope != null) ? childScope : "";
781 this.childScope = (childScope != null) ? childScope : "";
782 }
783
784 /**
785 * Resets this context for reuse, avoiding allocation of a new instance per derive() call.
786 */
787 private void reset(String parentScope, String childScope) {
788 this.parentScope = (parentScope != null) ? parentScope : "";
789 this.derivedScope = (childScope != null) ? childScope : "";
790 this.childScope = (childScope != null) ? childScope : "";
791 }
792
793 /**
794 * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
795 * the scope deriver.
796 *
797 * @return the scope of the parent dependency, never {@code null}
798 */
799 public String getParentScope() {
800 return parentScope;
801 }
802
803 /**
804 * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
805 * descriptor of the parent dependency.
806 *
807 * @return the original scope of the child dependency, never {@code null}
808 */
809 public String getChildScope() {
810 return childScope;
811 }
812
813 /**
814 * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
815 * scope deriver makes changes.
816 *
817 * @return the derived scope of the child dependency, never {@code null}
818 */
819 public String getDerivedScope() {
820 return derivedScope;
821 }
822
823 /**
824 * Sets the derived scope of the child dependency.
825 *
826 * @param derivedScope the derived scope of the dependency, may be {@code null}
827 */
828 public void setDerivedScope(String derivedScope) {
829 this.derivedScope = (derivedScope != null) ? derivedScope : "";
830 }
831 }
832
833 /**
834 * A conflicting dependency.
835 *
836 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
837 * change without notice and only exists to enable unit testing
838 */
839 private static final class ConflictItem extends ConflictResolver.ConflictItem {
840 private final Path path;
841 private final List<DependencyNode> parent;
842 private final Artifact artifact;
843 private final DependencyNode node;
844 private final int depth;
845 private final String scope;
846 private final int optionalities;
847
848 private ConflictItem(Path path) {
849 this.path = path;
850 if (path.parent != null) {
851 DependencyNode parent = path.parent.dn;
852 this.parent = parent.getChildren();
853 this.artifact = parent.getArtifact();
854 } else {
855 this.parent = null;
856 this.artifact = null;
857 }
858 this.node = path.dn;
859 this.depth = path.depth;
860 this.scope = path.scope;
861 this.optionalities = path.optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
862 }
863
864 /**
865 * Determines whether the specified conflict item is a sibling of this item.
866 *
867 * @param item the other conflict item, must not be {@code null}
868 * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise
869 */
870 @Override
871 public boolean isSibling(ConflictResolver.ConflictItem item) {
872 return parent == ((ConflictItem) item).parent;
873 }
874
875 /**
876 * Gets the dependency node involved in the conflict.
877 *
878 * @return the involved dependency node, never {@code null}
879 */
880 @Override
881 public DependencyNode getNode() {
882 return node;
883 }
884
885 /**
886 * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
887 *
888 * @return the involved dependency, never {@code null}
889 */
890 @Override
891 public Dependency getDependency() {
892 return node.getDependency();
893 }
894
895 /**
896 * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
897 * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
898 * possible depth.
899 *
900 * @return the zero-based depth of the node in the graph
901 */
902 @Override
903 public int getDepth() {
904 return depth;
905 }
906
907 /**
908 * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
909 * different paths and each path might result in a different derived scope.
910 *
911 * @return the (read-only) set of derived scopes of the dependency, never {@code null}
912 * @see ConflictResolver.ScopeDeriver
913 */
914 @Override
915 public Collection<String> getScopes() {
916 return Collections.singleton(scope);
917 }
918
919 /**
920 * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
921 * different paths and each path might result in a different derived optionality.
922 *
923 * @return a bit field consisting of {@link PathConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
924 * {@link PathConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
925 * dependency was encountered with
926 */
927 @Override
928 public int getOptionalities() {
929 return optionalities;
930 }
931
932 @Override
933 public String toString() {
934 return node + " @ " + depth + " < " + artifact;
935 }
936 }
937
938 /**
939 * A context used to hold information that is relevant for resolving version and scope conflicts.
940 *
941 * @see ConflictResolver.VersionSelector
942 * @see ConflictResolver.ScopeSelector
943 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
944 * change without notice and only exists to enable unit testing
945 */
946 private static final class ConflictContext extends ConflictResolver.ConflictContext {
947 private final DependencyNode root;
948 private final Map<DependencyNode, String> conflictIds;
949 private final Collection<ConflictResolver.ConflictItem> items;
950 private final String conflictId;
951
952 // elected properties
953 private ConflictItem winner;
954 private String scope;
955 private Boolean optional;
956
957 private ConflictContext(
958 DependencyNode root,
959 Map<DependencyNode, String> conflictIds,
960 Collection<ConflictItem> items,
961 String conflictId) {
962 this.root = root;
963 this.conflictIds = conflictIds;
964 this.items = Collections.unmodifiableCollection(items);
965 this.conflictId = conflictId;
966 }
967
968 /**
969 * Gets the root node of the dependency graph being transformed.
970 *
971 * @return the root node of the dependency graph, never {@code null}
972 */
973 @Override
974 public DependencyNode getRoot() {
975 return root;
976 }
977
978 /**
979 * Determines whether the specified dependency node belongs to this conflict context.
980 *
981 * @param node the dependency node to check, must not be {@code null}
982 * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise
983 */
984 @Override
985 public boolean isIncluded(DependencyNode node) {
986 return conflictId.equals(conflictIds.get(node));
987 }
988
989 /**
990 * Gets the collection of conflict items in this context.
991 *
992 * @return the (read-only) collection of conflict items in this context, never {@code null}
993 */
994 @Override
995 public Collection<ConflictResolver.ConflictItem> getItems() {
996 return items;
997 }
998
999 /**
1000 * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
1001 *
1002 * @return the winning conflict item or {@code null} if not set yet
1003 */
1004 @Override
1005 public ConflictResolver.ConflictItem getWinner() {
1006 return winner;
1007 }
1008
1009 /**
1010 * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
1011 *
1012 * @param winner the winning conflict item, may be {@code null}
1013 */
1014 @Override
1015 public void setWinner(ConflictResolver.ConflictItem winner) {
1016 this.winner = (ConflictItem) winner;
1017 }
1018
1019 /**
1020 * Gets the effective scope of the winning dependency.
1021 *
1022 * @return the effective scope of the winning dependency or {@code null} if none
1023 */
1024 @Override
1025 public String getScope() {
1026 return scope;
1027 }
1028
1029 /**
1030 * Sets the effective scope of the winning dependency.
1031 *
1032 * @param scope the effective scope, may be {@code null}
1033 */
1034 @Override
1035 public void setScope(String scope) {
1036 this.scope = scope;
1037 }
1038
1039 /**
1040 * Gets the effective optional flag of the winning dependency.
1041 *
1042 * @return the effective optional flag or {@code null} if none
1043 */
1044 @Override
1045 public Boolean getOptional() {
1046 return optional;
1047 }
1048
1049 /**
1050 * Sets the effective optional flag of the winning dependency.
1051 *
1052 * @param optional the effective optional flag, may be {@code null}
1053 */
1054 @Override
1055 public void setOptional(Boolean optional) {
1056 this.optional = optional;
1057 }
1058
1059 @Override
1060 public String toString() {
1061 return winner + " @ " + scope + " < " + items;
1062 }
1063 }
1064 }