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