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