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