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.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029import java.util.Set; 030 031import org.eclipse.aether.ConfigurationProperties; 032import org.eclipse.aether.RepositoryException; 033import org.eclipse.aether.RepositorySystemSession; 034import org.eclipse.aether.artifact.Artifact; 035import org.eclipse.aether.collection.DependencyGraphTransformationContext; 036import org.eclipse.aether.graph.DefaultDependencyNode; 037import org.eclipse.aether.graph.Dependency; 038import org.eclipse.aether.graph.DependencyNode; 039import org.eclipse.aether.util.ConfigUtils; 040import org.eclipse.aether.util.artifact.ArtifactIdUtils; 041 042import static java.util.Objects.requireNonNull; 043 044/** 045 * A high-performance dependency graph transformer that resolves version and scope conflicts among dependencies. 046 * This is the recommended conflict resolver implementation that provides O(N) performance characteristics, 047 * significantly improving upon the O(N²) worst-case performance of {@link ClassicConflictResolver}. 048 * <p> 049 * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled 050 * depends on the configured verbosity level: they may be removed entirely, have their children removed, or 051 * be left in place with conflict information. The exact rules by which a winning node and its effective scope 052 * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector}, 053 * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}. 054 * <p> 055 * <strong>Performance Characteristics:</strong> 056 * <ul> 057 * <li><strong>Time Complexity:</strong> O(N) where N is the number of dependency nodes</li> 058 * <li><strong>Memory Usage:</strong> Creates a parallel tree structure for conflict-free processing</li> 059 * <li><strong>Scalability:</strong> Excellent performance on large multi-module projects</li> 060 * </ul> 061 * <p> 062 * <strong>Algorithm Overview:</strong> 063 * <ol> 064 * <li><strong>Path Tree Construction:</strong> Builds a cycle-free parallel tree structure from the input 065 * dependency graph, where each {@code Path} represents a unique route to a dependency node</li> 066 * <li><strong>Conflict Partitioning:</strong> Groups paths by conflict ID (based on groupId:artifactId:classifier:extension coordinates)</li> 067 * <li><strong>Topological Processing:</strong> Processes conflict groups in topologically sorted order</li> 068 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners within each conflict group</li> 069 * <li><strong>Graph Transformation:</strong> Applies changes back to the original dependency graph</li> 070 * </ol> 071 * <p> 072 * <strong>Key Differences from {@link ClassicConflictResolver}:</strong> 073 * <ul> 074 * <li><strong>Performance:</strong> O(N) vs O(N²) time complexity</li> 075 * <li><strong>Memory Strategy:</strong> Uses parallel tree structure vs in-place graph modification</li> 076 * <li><strong>Cycle Handling:</strong> Explicitly breaks cycles during tree construction</li> 077 * <li><strong>Processing Order:</strong> Level-by-level from root vs depth-first traversal</li> 078 * </ul> 079 * <p> 080 * <strong>When to Use:</strong> 081 * <ul> 082 * <li>Default choice for all new projects and Maven 4+ installations</li> 083 * <li>Large multi-module projects with many dependencies</li> 084 * <li>Performance-critical build environments</li> 085 * <li>Any scenario where {@link ClassicConflictResolver} shows performance bottlenecks</li> 086 * </ul> 087 * <p> 088 * <strong>Implementation Note:</strong> This conflict resolver builds a cycle-free "parallel" structure based on the 089 * passed-in dependency graph, and applies operations level by level starting from the root. The parallel {@code Path} 090 * tree ensures that cycles in the original graph don't affect the conflict resolution algorithm's performance. 091 * 092 * @see ClassicConflictResolver 093 * @since 2.0.11 094 */ 095public final class PathConflictResolver extends ConflictResolver { 096 /** 097 * This implementation of conflict resolver is able to show more precise information regarding cycles in standard 098 * verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it 099 * 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}