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