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