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