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.IdentityHashMap; 027import java.util.Iterator; 028import java.util.List; 029import java.util.ListIterator; 030import java.util.Map; 031import java.util.Objects; 032 033import org.eclipse.aether.RepositoryException; 034import org.eclipse.aether.artifact.Artifact; 035import org.eclipse.aether.collection.DependencyGraphTransformationContext; 036import org.eclipse.aether.graph.DefaultDependencyNode; 037import org.eclipse.aether.graph.Dependency; 038import org.eclipse.aether.graph.DependencyNode; 039import org.eclipse.aether.util.artifact.ArtifactIdUtils; 040 041import static java.util.Objects.requireNonNull; 042 043/** 044 * A legacy dependency graph transformer that resolves version and scope conflicts among dependencies. 045 * This implementation maintains backward compatibility with Maven 3.x and Resolver 1.x behavior but has 046 * O(N²) worst-case performance characteristics. For new projects, consider using {@link PathConflictResolver}. 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>Performance Characteristics:</strong> 055 * <ul> 056 * <li><strong>Time Complexity:</strong> O(N²) worst-case where N is the number of dependency nodes</li> 057 * <li><strong>Memory Usage:</strong> Modifies the dependency graph in-place</li> 058 * <li><strong>Scalability:</strong> Performance degrades significantly on large multi-module projects</li> 059 * </ul> 060 * <p> 061 * <strong>Algorithm Overview:</strong> 062 * <ol> 063 * <li><strong>Depth-First Traversal:</strong> Walks the dependency graph depth-first</li> 064 * <li><strong>In-Place Modification:</strong> Modifies nodes directly during traversal</li> 065 * <li><strong>Conflict Detection:</strong> Identifies conflicts by comparing conflict IDs</li> 066 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners</li> 067 * <li><strong>Loser Removal:</strong> Removes or marks losing nodes during traversal</li> 068 * </ol> 069 * <p> 070 * <strong>When to Use:</strong> 071 * <ul> 072 * <li>Exact backward compatibility with Maven 3.x/Resolver 1.x is required</li> 073 * <li>Debugging performance differences between old and new algorithms</li> 074 * <li>Small projects where performance is not a concern</li> 075 * <li>Testing and validation scenarios</li> 076 * </ul> 077 * <p> 078 * <strong>Migration Recommendation:</strong> New projects should use {@link PathConflictResolver} for better 079 * performance. This implementation is retained primarily for compatibility and testing purposes. 080 * <p> 081 * <strong>Implementation Note:</strong> This conflict resolver is identical to the one used in Maven 3/Resolver 1.x. 082 * The implementation may produce O(N²) worst-case performance on projects with many small conflict groups 083 * (typically one member each), which is common in large multi-module projects. 084 * 085 * @see PathConflictResolver 086 * @since 2.0.11 087 */ 088public final class ClassicConflictResolver extends ConflictResolver { 089 private final ConflictResolver.VersionSelector versionSelector; 090 private final ConflictResolver.ScopeSelector scopeSelector; 091 private final ConflictResolver.ScopeDeriver scopeDeriver; 092 private final ConflictResolver.OptionalitySelector optionalitySelector; 093 094 /** 095 * Creates a new conflict resolver instance with the specified hooks. 096 * 097 * @param versionSelector The version selector to use, must not be {@code null}. 098 * @param scopeSelector The scope selector to use, must not be {@code null}. 099 * @param optionalitySelector The optionality selector ot use, must not be {@code null}. 100 * @param scopeDeriver The scope deriver to use, must not be {@code null}. 101 */ 102 public ClassicConflictResolver( 103 ConflictResolver.VersionSelector versionSelector, 104 ConflictResolver.ScopeSelector scopeSelector, 105 ConflictResolver.OptionalitySelector optionalitySelector, 106 ConflictResolver.ScopeDeriver scopeDeriver) { 107 this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null"); 108 this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null"); 109 this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null"); 110 this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null"); 111 } 112 113 @SuppressWarnings("unchecked") 114 @Override 115 public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context) 116 throws RepositoryException { 117 requireNonNull(node, "node cannot be null"); 118 requireNonNull(context, "context cannot be null"); 119 List<String> sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS); 120 if (sortedConflictIds == null) { 121 ConflictIdSorter sorter = new ConflictIdSorter(); 122 sorter.transformGraph(node, context); 123 124 sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS); 125 } 126 127 @SuppressWarnings("unchecked") 128 Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS); 129 long time1 = System.nanoTime(); 130 131 @SuppressWarnings("unchecked") 132 Collection<Collection<String>> conflictIdCycles = 133 (Collection<Collection<String>>) context.get(TransformationContextKeys.CYCLIC_CONFLICT_IDS); 134 if (conflictIdCycles == null) { 135 throw new RepositoryException("conflict id cycles have not been identified"); 136 } 137 138 Map<DependencyNode, String> conflictIds = 139 (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS); 140 if (conflictIds == null) { 141 throw new RepositoryException("conflict groups have not been identified"); 142 } 143 144 Map<String, Collection<String>> cyclicPredecessors = new HashMap<>(); 145 for (Collection<String> cycle : conflictIdCycles) { 146 for (String conflictId : cycle) { 147 Collection<String> predecessors = cyclicPredecessors.computeIfAbsent(conflictId, k -> new HashSet<>()); 148 predecessors.addAll(cycle); 149 } 150 } 151 152 State state = new State(node, conflictIds, sortedConflictIds.size(), context); 153 for (Iterator<String> it = sortedConflictIds.iterator(); it.hasNext(); ) { 154 String conflictId = it.next(); 155 156 // reset data structures for next graph walk 157 state.prepare(conflictId, cyclicPredecessors.get(conflictId)); 158 159 // find nodes with the current conflict id and while walking the graph (more deeply), nuke leftover losers 160 gatherConflictItems(node, state); 161 162 // now that we know the min depth of the parents, update depth of conflict items 163 state.finish(); 164 165 // earlier runs might have nuked all parents of the current conflict id, so it might not exist anymore 166 if (!state.items.isEmpty()) { 167 ConflictContext ctx = state.conflictCtx; 168 state.versionSelector.selectVersion(ctx); 169 if (ctx.winner == null) { 170 throw new RepositoryException("conflict resolver did not select winner among " + state.items); 171 } 172 DependencyNode winner = ((ConflictItem) (ctx.winner)).node; 173 174 state.scopeSelector.selectScope(ctx); 175 if (ConflictResolver.Verbosity.NONE != state.verbosity) { 176 winner.setData( 177 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE, 178 winner.getDependency().getScope()); 179 } 180 winner.setScope(ctx.scope); 181 182 state.optionalitySelector.selectOptionality(ctx); 183 if (ConflictResolver.Verbosity.NONE != state.verbosity) { 184 winner.setData( 185 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY, 186 winner.getDependency().isOptional()); 187 } 188 winner.setOptional(ctx.optional); 189 190 removeLosers(state); 191 } 192 193 // record the winner so we can detect leftover losers during future graph walks 194 state.winner(); 195 196 // in case of cycles, trigger final graph walk to ensure all leftover losers are gone 197 if (!it.hasNext() && !conflictIdCycles.isEmpty() && state.conflictCtx.winner != null) { 198 DependencyNode winner = ((ConflictItem) (state.conflictCtx).winner).node; 199 // Note: using non-existing key here (empty) as that one for sure was not met 200 state.prepare("", null); 201 gatherConflictItems(winner, state); 202 } 203 } 204 205 if (stats != null) { 206 long time2 = System.nanoTime(); 207 stats.put("ConflictResolver.totalTime", time2 - time1); 208 stats.put("ConflictResolver.conflictItemCount", state.totalConflictItems); 209 } 210 211 return node; 212 } 213 214 private boolean gatherConflictItems(DependencyNode node, State state) throws RepositoryException { 215 String conflictId = state.conflictIds.get(node); 216 if (state.currentId.equals(conflictId)) { 217 // found it, add conflict item (if not already done earlier by another path) 218 state.add(node); 219 // we don't recurse here so we might miss losers beneath us, those will be nuked during future walks below 220 } else if (state.loser(node, conflictId)) { 221 // found a leftover loser (likely in a cycle) of an already processed conflict id, tell caller to nuke it 222 return false; 223 } else if (state.push(node, conflictId)) { 224 // found potential parent, no cycle and not visited before with the same derived scope, so recurse 225 for (Iterator<DependencyNode> it = node.getChildren().iterator(); it.hasNext(); ) { 226 DependencyNode child = it.next(); 227 if (!gatherConflictItems(child, state)) { 228 it.remove(); 229 } 230 } 231 state.pop(); 232 } 233 return true; 234 } 235 236 private static void removeLosers(State state) { 237 ConflictItem winner = ((ConflictItem) state.conflictCtx.winner); 238 String winnerArtifactId = ArtifactIdUtils.toId(winner.node.getArtifact()); 239 List<DependencyNode> previousParent = null; 240 ListIterator<DependencyNode> childIt = null; 241 HashSet<String> toRemoveIds = new HashSet<>(); 242 for (ConflictItem item : state.items) { 243 if (item == winner) { 244 continue; 245 } 246 if (item.parent != previousParent) { 247 childIt = item.parent.listIterator(); 248 previousParent = item.parent; 249 } 250 while (childIt.hasNext()) { 251 DependencyNode child = childIt.next(); 252 if (child == item.node) { 253 // NONE: just remove it and done 254 if (ConflictResolver.Verbosity.NONE == state.verbosity) { 255 childIt.remove(); 256 break; 257 } 258 259 // STANDARD: doing extra bookkeeping to select "which nodes to remove" 260 if (ConflictResolver.Verbosity.STANDARD == state.verbosity) { 261 String childArtifactId = ArtifactIdUtils.toId(child.getArtifact()); 262 // if two IDs are equal, it means "there is nearest", not conflict per se. 263 // In that case we do NOT allow this child to be removed (but remove others) 264 // and this keeps us safe from iteration (and in general, version) ordering 265 // as we explicitly leave out ID that is "nearest found" state. 266 // 267 // This tackles version ranges mostly, where ranges are turned into list of 268 // several nodes in collector (as many were discovered, ie. from metadata), and 269 // old code would just "mark" the first hit as conflict, and remove the rest, 270 // even if rest could contain "more suitable" version, that is not conflicting/diverging. 271 // This resulted in verbose mode transformed tree, that was misrepresenting things 272 // for dependency convergence calculations: it represented state like parent node 273 // depends on "wrong" version (diverge), while "right" version was present (but removed) 274 // as well, as it was contained in parents version range. 275 if (!Objects.equals(winnerArtifactId, childArtifactId)) { 276 toRemoveIds.add(childArtifactId); 277 } 278 } 279 280 // FULL: just record the facts 281 DependencyNode loser = new DefaultDependencyNode(child); 282 loser.setData(ConflictResolver.NODE_DATA_WINNER, winner.node); 283 loser.setData( 284 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE, 285 loser.getDependency().getScope()); 286 loser.setData( 287 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY, 288 loser.getDependency().isOptional()); 289 loser.setScope(item.getScopes().iterator().next()); 290 loser.setChildren(Collections.emptyList()); 291 childIt.set(loser); 292 item.node = loser; 293 break; 294 } 295 } 296 } 297 298 // 2nd pass to apply "standard" verbosity: leaving only 1 loser, but with care 299 if (ConflictResolver.Verbosity.STANDARD == state.verbosity && !toRemoveIds.isEmpty()) { 300 previousParent = null; 301 for (ConflictItem item : state.items) { 302 if (item == winner) { 303 continue; 304 } 305 if (item.parent != previousParent) { 306 childIt = item.parent.listIterator(); 307 previousParent = item.parent; 308 } 309 while (childIt.hasNext()) { 310 DependencyNode child = childIt.next(); 311 if (child == item.node) { 312 String childArtifactId = ArtifactIdUtils.toId(child.getArtifact()); 313 if (toRemoveIds.contains(childArtifactId) 314 && relatedSiblingsCount(child.getArtifact(), item.parent) > 1) { 315 childIt.remove(); 316 } 317 break; 318 } 319 } 320 } 321 } 322 323 // there might still be losers beneath the winner (e.g. in case of cycles) 324 // those will be nuked during future graph walks when we include the winner in the recursion 325 } 326 327 private static long relatedSiblingsCount(Artifact artifact, List<DependencyNode> parent) { 328 String ga = artifact.getGroupId() + ":" + artifact.getArtifactId(); 329 return parent.stream() 330 .map(DependencyNode::getArtifact) 331 .filter(a -> ga.equals(a.getGroupId() + ":" + a.getArtifactId())) 332 .count(); 333 } 334 335 static final class NodeInfo { 336 337 /** 338 * The smallest depth at which the node was seen, used for "the" depth of its conflict items. 339 */ 340 int minDepth; 341 342 /** 343 * The set of derived scopes the node was visited with, used to check whether an already seen node needs to be 344 * revisited again in context of another scope. To conserve memory, we start with {@code String} and update to 345 * {@code Set<String>} if needed. 346 */ 347 Object derivedScopes; 348 349 /** 350 * The set of derived optionalities the node was visited with, used to check whether an already seen node needs 351 * to be revisited again in context of another optionality. To conserve memory, encoded as bit field (bit 0 -> 352 * optional=false, bit 1 -> optional=true). 353 */ 354 int derivedOptionalities; 355 356 /** 357 * The conflict items which are immediate children of the node, used to easily update those conflict items after 358 * a new parent scope/optionality was encountered. 359 */ 360 List<ConflictItem> children; 361 362 static final int CHANGE_SCOPE = 0x01; 363 364 static final int CHANGE_OPTIONAL = 0x02; 365 366 private static final int OPT_FALSE = 0x01; 367 368 private static final int OPT_TRUE = 0x02; 369 370 NodeInfo(int depth, String derivedScope, boolean optional) { 371 minDepth = depth; 372 derivedScopes = derivedScope; 373 derivedOptionalities = optional ? OPT_TRUE : OPT_FALSE; 374 } 375 376 @SuppressWarnings("unchecked") 377 int update(int depth, String derivedScope, boolean optional) { 378 if (depth < minDepth) { 379 minDepth = depth; 380 } 381 int changes; 382 if (derivedScopes.equals(derivedScope)) { 383 changes = 0; 384 } else if (derivedScopes instanceof Collection) { 385 changes = ((Collection<String>) derivedScopes).add(derivedScope) ? CHANGE_SCOPE : 0; 386 } else { 387 Collection<String> scopes = new HashSet<>(); 388 scopes.add((String) derivedScopes); 389 scopes.add(derivedScope); 390 derivedScopes = scopes; 391 changes = CHANGE_SCOPE; 392 } 393 int bit = optional ? OPT_TRUE : OPT_FALSE; 394 if ((derivedOptionalities & bit) == 0) { 395 derivedOptionalities |= bit; 396 changes |= CHANGE_OPTIONAL; 397 } 398 return changes; 399 } 400 401 void add(ConflictItem item) { 402 if (children == null) { 403 children = new ArrayList<>(1); 404 } 405 children.add(item); 406 } 407 } 408 409 final class State { 410 411 /** 412 * The conflict id currently processed. 413 */ 414 String currentId; 415 416 /** 417 * Stats counter. 418 */ 419 int totalConflictItems; 420 421 /** 422 * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts. 423 */ 424 final ConflictResolver.Verbosity verbosity; 425 426 /** 427 * A mapping from conflict id to winner node, helps to recognize nodes that have their effective 428 * scope&optionality set or are leftovers from previous removals. 429 */ 430 final Map<String, DependencyNode> resolvedIds; 431 432 /** 433 * The set of conflict ids which could apply to ancestors of nodes with the current conflict id, used to avoid 434 * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account 435 * for cyclic dependencies. 436 */ 437 final Collection<String> potentialAncestorIds; 438 439 /** 440 * The output from the conflict marker 441 */ 442 final Map<DependencyNode, String> conflictIds; 443 444 /** 445 * The conflict items we have gathered so far for the current conflict id. 446 */ 447 final List<ConflictItem> items; 448 449 /** 450 * The (conceptual) mapping from nodes to extra infos, technically keyed by the node's child list which better 451 * captures the identity of a node since we're basically concerned with effects towards children. 452 */ 453 final Map<List<DependencyNode>, NodeInfo> infos; 454 455 /** 456 * The set of nodes on the DFS stack to detect cycles, technically keyed by the node's child list to match the 457 * dirty graph structure produced by the dependency collector for cycles. 458 */ 459 final Map<List<DependencyNode>, Boolean> stack; 460 461 /** 462 * The stack of parent nodes. 463 */ 464 final List<DependencyNode> parentNodes; 465 466 /** 467 * The stack of derived scopes for parent nodes. 468 */ 469 final List<String> parentScopes; 470 471 /** 472 * The stack of derived optional flags for parent nodes. 473 */ 474 final List<Boolean> parentOptionals; 475 476 /** 477 * The stack of node infos for parent nodes, may contain {@code null} which is used to disable creating new 478 * conflict items when visiting their parent again (conflict items are meant to be unique by parent-node combo). 479 */ 480 final List<NodeInfo> parentInfos; 481 482 /** 483 * The conflict context passed to the version/scope/optionality selectors, updated as we move along rather than 484 * recreated to avoid tmp objects. 485 */ 486 final ConflictContext conflictCtx; 487 488 /** 489 * The scope context passed to the scope deriver, updated as we move along rather than recreated to avoid tmp 490 * objects. 491 */ 492 final ScopeContext scopeCtx; 493 494 /** 495 * The effective version selector, i.e. after initialization. 496 */ 497 final ConflictResolver.VersionSelector versionSelector; 498 499 /** 500 * The effective scope selector, i.e. after initialization. 501 */ 502 final ConflictResolver.ScopeSelector scopeSelector; 503 504 /** 505 * The effective scope deriver, i.e. after initialization. 506 */ 507 final ConflictResolver.ScopeDeriver scopeDeriver; 508 509 /** 510 * The effective optionality selector, i.e. after initialization. 511 */ 512 final ConflictResolver.OptionalitySelector optionalitySelector; 513 514 State( 515 DependencyNode root, 516 Map<DependencyNode, String> conflictIds, 517 int conflictIdCount, 518 DependencyGraphTransformationContext context) 519 throws RepositoryException { 520 this.conflictIds = conflictIds; 521 this.verbosity = ConflictResolver.getVerbosity(context.getSession()); 522 potentialAncestorIds = new HashSet<>(conflictIdCount * 2); 523 resolvedIds = new HashMap<>(conflictIdCount * 2); 524 items = new ArrayList<>(256); 525 infos = new IdentityHashMap<>(64); 526 stack = new IdentityHashMap<>(64); 527 parentNodes = new ArrayList<>(64); 528 parentScopes = new ArrayList<>(64); 529 parentOptionals = new ArrayList<>(64); 530 parentInfos = new ArrayList<>(64); 531 conflictCtx = new ConflictContext(root, conflictIds, items); 532 scopeCtx = new ScopeContext(null, null); 533 versionSelector = ClassicConflictResolver.this.versionSelector.getInstance(root, context); 534 scopeSelector = ClassicConflictResolver.this.scopeSelector.getInstance(root, context); 535 scopeDeriver = ClassicConflictResolver.this.scopeDeriver.getInstance(root, context); 536 optionalitySelector = ClassicConflictResolver.this.optionalitySelector.getInstance(root, context); 537 } 538 539 void prepare(String conflictId, Collection<String> cyclicPredecessors) { 540 currentId = conflictId; 541 conflictCtx.conflictId = conflictId; 542 conflictCtx.winner = null; 543 conflictCtx.scope = null; 544 conflictCtx.optional = null; 545 items.clear(); 546 infos.clear(); 547 if (cyclicPredecessors != null) { 548 potentialAncestorIds.addAll(cyclicPredecessors); 549 } 550 } 551 552 void finish() { 553 List<DependencyNode> previousParent = null; 554 int previousDepth = 0; 555 totalConflictItems += items.size(); 556 for (ListIterator<ConflictItem> iterator = items.listIterator(items.size()); iterator.hasPrevious(); ) { 557 ConflictItem item = iterator.previous(); 558 if (item.parent == previousParent) { 559 item.depth = previousDepth; 560 } else if (item.parent != null) { 561 previousParent = item.parent; 562 NodeInfo info = infos.get(previousParent); 563 previousDepth = info.minDepth + 1; 564 item.depth = previousDepth; 565 } 566 } 567 potentialAncestorIds.add(currentId); 568 } 569 570 void winner() { 571 resolvedIds.put(currentId, (conflictCtx.winner != null) ? ((ConflictItem) conflictCtx.winner).node : null); 572 } 573 574 boolean loser(DependencyNode node, String conflictId) { 575 DependencyNode winner = resolvedIds.get(conflictId); 576 return winner != null && winner != node; 577 } 578 579 boolean push(DependencyNode node, String conflictId) throws RepositoryException { 580 if (conflictId == null) { 581 if (node.getDependency() != null) { 582 if (node.getData().get(ConflictResolver.NODE_DATA_WINNER) != null) { 583 return false; 584 } 585 throw new RepositoryException("missing conflict id for node " + node); 586 } 587 } else if (!potentialAncestorIds.contains(conflictId)) { 588 return false; 589 } 590 591 List<DependencyNode> graphNode = node.getChildren(); 592 if (stack.put(graphNode, Boolean.TRUE) != null) { 593 return false; 594 } 595 596 int depth = depth(); 597 String scope = deriveScope(node, conflictId); 598 boolean optional = deriveOptional(node, conflictId); 599 NodeInfo info = infos.get(graphNode); 600 if (info == null) { 601 info = new NodeInfo(depth, scope, optional); 602 infos.put(graphNode, info); 603 parentInfos.add(info); 604 parentNodes.add(node); 605 parentScopes.add(scope); 606 parentOptionals.add(optional); 607 } else { 608 int changes = info.update(depth, scope, optional); 609 if (changes == 0) { 610 stack.remove(graphNode); 611 return false; 612 } 613 parentInfos.add(null); // disable creating new conflict items, we update the existing ones below 614 parentNodes.add(node); 615 parentScopes.add(scope); 616 parentOptionals.add(optional); 617 if (info.children != null) { 618 if ((changes & NodeInfo.CHANGE_SCOPE) != 0) { 619 ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size()); 620 while (itemIterator.hasPrevious()) { 621 ConflictItem item = itemIterator.previous(); 622 String childScope = deriveScope(item.node, null); 623 item.addScope(childScope); 624 } 625 } 626 if ((changes & NodeInfo.CHANGE_OPTIONAL) != 0) { 627 ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size()); 628 while (itemIterator.hasPrevious()) { 629 ConflictItem item = itemIterator.previous(); 630 boolean childOptional = deriveOptional(item.node, null); 631 item.addOptional(childOptional); 632 } 633 } 634 } 635 } 636 637 return true; 638 } 639 640 void pop() { 641 int last = parentInfos.size() - 1; 642 parentInfos.remove(last); 643 parentScopes.remove(last); 644 parentOptionals.remove(last); 645 DependencyNode node = parentNodes.remove(last); 646 stack.remove(node.getChildren()); 647 } 648 649 void add(DependencyNode node) throws RepositoryException { 650 DependencyNode parent = parent(); 651 if (parent == null) { 652 ConflictItem item = newConflictItem(parent, node); 653 items.add(item); 654 } else { 655 NodeInfo info = parentInfos.get(parentInfos.size() - 1); 656 if (info != null) { 657 ConflictItem item = newConflictItem(parent, node); 658 info.add(item); 659 items.add(item); 660 } 661 } 662 } 663 664 private ConflictItem newConflictItem(DependencyNode parent, DependencyNode node) throws RepositoryException { 665 return new ConflictItem(parent, node, deriveScope(node, null), deriveOptional(node, null)); 666 } 667 668 private int depth() { 669 return parentNodes.size(); 670 } 671 672 private DependencyNode parent() { 673 int size = parentNodes.size(); 674 return (size <= 0) ? null : parentNodes.get(size - 1); 675 } 676 677 private String deriveScope(DependencyNode node, String conflictId) throws RepositoryException { 678 if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0 679 || (conflictId != null && resolvedIds.containsKey(conflictId))) { 680 return scope(node.getDependency()); 681 } 682 683 int depth = parentNodes.size(); 684 scopes(depth, node.getDependency()); 685 if (depth > 0) { 686 scopeDeriver.deriveScope(scopeCtx); 687 } 688 return scopeCtx.derivedScope; 689 } 690 691 private void scopes(int parent, Dependency child) { 692 scopeCtx.parentScope = (parent > 0) ? parentScopes.get(parent - 1) : null; 693 scopeCtx.derivedScope = scope(child); 694 scopeCtx.childScope = scope(child); 695 } 696 697 private String scope(Dependency dependency) { 698 return (dependency != null) ? dependency.getScope() : null; 699 } 700 701 private boolean deriveOptional(DependencyNode node, String conflictId) { 702 Dependency dep = node.getDependency(); 703 boolean optional = (dep != null) && dep.isOptional(); 704 if (optional 705 || (node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0 706 || (conflictId != null && resolvedIds.containsKey(conflictId))) { 707 return optional; 708 } 709 int depth = parentNodes.size(); 710 return (depth > 0) ? parentOptionals.get(depth - 1) : false; 711 } 712 } 713 714 /** 715 * A context used to hold information that is relevant for deriving the scope of a child dependency. 716 * 717 * @see ConflictResolver.ScopeDeriver 718 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may 719 * change without notice and only exists to enable unit testing. 720 */ 721 private static final class ScopeContext extends ConflictResolver.ScopeContext { 722 private String parentScope; 723 private String childScope; 724 private String derivedScope; 725 726 /** 727 * Creates a new scope context with the specified properties. 728 * 729 * @param parentScope The scope of the parent dependency, may be {@code null}. 730 * @param childScope The scope of the child dependency, may be {@code null}. 731 * @noreference This class is not intended to be instantiated by clients in production code, the constructor may 732 * change without notice and only exists to enable unit testing. 733 */ 734 private ScopeContext(String parentScope, String childScope) { 735 this.parentScope = (parentScope != null) ? parentScope : ""; 736 derivedScope = (childScope != null) ? childScope : ""; 737 this.childScope = (childScope != null) ? childScope : ""; 738 } 739 740 /** 741 * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of 742 * the scope deriver. 743 * 744 * @return The scope of the parent dependency, never {@code null}. 745 */ 746 @Override 747 public String getParentScope() { 748 return parentScope; 749 } 750 751 /** 752 * Gets the original scope of the child dependency. This is the scope that was declared in the artifact 753 * descriptor of the parent dependency. 754 * 755 * @return The original scope of the child dependency, never {@code null}. 756 */ 757 @Override 758 public String getChildScope() { 759 return childScope; 760 } 761 762 /** 763 * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the 764 * scope deriver makes changes. 765 * 766 * @return The derived scope of the child dependency, never {@code null}. 767 */ 768 @Override 769 public String getDerivedScope() { 770 return derivedScope; 771 } 772 773 /** 774 * Sets the derived scope of the child dependency. 775 * 776 * @param derivedScope The derived scope of the dependency, may be {@code null}. 777 */ 778 @Override 779 public void setDerivedScope(String derivedScope) { 780 this.derivedScope = (derivedScope != null) ? derivedScope : ""; 781 } 782 } 783 784 /** 785 * A conflicting dependency. 786 * 787 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may 788 * change without notice and only exists to enable unit testing. 789 */ 790 private static final class ConflictItem extends ConflictResolver.ConflictItem { 791 792 // nodes can share child lists, we care about the unique owner of a child node which is the child list 793 final List<DependencyNode> parent; 794 795 // only for debugging/toString() to help identify the parent node(s) 796 final Artifact artifact; 797 798 // is mutable as removeLosers will mutate it (if Verbosity==STANDARD) 799 DependencyNode node; 800 801 int depth; 802 803 // we start with String and update to Set<String> if needed 804 Object scopes; 805 806 // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE 807 int optionalities; 808 809 /** 810 * Bit flag indicating whether one or more paths consider the dependency non-optional. 811 */ 812 public static final int OPTIONAL_FALSE = 0x01; 813 814 /** 815 * Bit flag indicating whether one or more paths consider the dependency optional. 816 */ 817 public static final int OPTIONAL_TRUE = 0x02; 818 819 private ConflictItem(DependencyNode parent, DependencyNode node, String scope, boolean optional) { 820 if (parent != null) { 821 this.parent = parent.getChildren(); 822 this.artifact = parent.getArtifact(); 823 } else { 824 this.parent = null; 825 this.artifact = null; 826 } 827 this.node = node; 828 this.scopes = scope; 829 this.optionalities = optional ? OPTIONAL_TRUE : OPTIONAL_FALSE; 830 } 831 832 /** 833 * Determines whether the specified conflict item is a sibling of this item. 834 * 835 * @param item The other conflict item, must not be {@code null}. 836 * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise. 837 */ 838 @Override 839 public boolean isSibling(ConflictResolver.ConflictItem item) { 840 return parent == ((ConflictItem) item).parent; 841 } 842 843 /** 844 * Gets the dependency node involved in the conflict. 845 * 846 * @return The involved dependency node, never {@code null}. 847 */ 848 @Override 849 public DependencyNode getNode() { 850 return node; 851 } 852 853 /** 854 * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}. 855 * 856 * @return The involved dependency, never {@code null}. 857 */ 858 @Override 859 public Dependency getDependency() { 860 return node.getDependency(); 861 } 862 863 /** 864 * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the 865 * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest 866 * possible depth. 867 * 868 * @return The zero-based depth of the node in the graph. 869 */ 870 @Override 871 public int getDepth() { 872 return depth; 873 } 874 875 /** 876 * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via 877 * different paths and each path might result in a different derived scope. 878 * 879 * @see ConflictResolver.ScopeDeriver 880 * @return The (read-only) set of derived scopes of the dependency, never {@code null}. 881 */ 882 @SuppressWarnings("unchecked") 883 @Override 884 public Collection<String> getScopes() { 885 if (scopes instanceof String) { 886 return Collections.singleton((String) scopes); 887 } 888 return (Collection<String>) scopes; 889 } 890 891 @SuppressWarnings("unchecked") 892 void addScope(String scope) { 893 if (scopes instanceof Collection) { 894 ((Collection<String>) scopes).add(scope); 895 } else if (!scopes.equals(scope)) { 896 Collection<String> set = new HashSet<>(); 897 set.add((String) scopes); 898 set.add(scope); 899 scopes = set; 900 } 901 } 902 903 /** 904 * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via 905 * different paths and each path might result in a different derived optionality. 906 * 907 * @return A bit field consisting of {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or 908 * {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the 909 * dependency was encountered with. 910 */ 911 @Override 912 public int getOptionalities() { 913 return optionalities; 914 } 915 916 void addOptional(boolean optional) { 917 optionalities |= optional ? OPTIONAL_TRUE : OPTIONAL_FALSE; 918 } 919 920 @Override 921 public String toString() { 922 return node + " @ " + depth + " < " + artifact; 923 } 924 } 925 926 /** 927 * A context used to hold information that is relevant for resolving version and scope conflicts. 928 * 929 * @see ConflictResolver.VersionSelector 930 * @see ConflictResolver.ScopeSelector 931 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may 932 * change without notice and only exists to enable unit testing. 933 */ 934 private static final class ConflictContext extends ConflictResolver.ConflictContext { 935 final DependencyNode root; 936 final Map<DependencyNode, String> conflictIds; 937 final Collection<ConflictResolver.ConflictItem> items; 938 939 String conflictId; 940 ConflictResolver.ConflictItem winner; 941 String scope; 942 Boolean optional; 943 944 private ConflictContext( 945 DependencyNode root, Map<DependencyNode, String> conflictIds, Collection<ConflictItem> items) { 946 this.root = root; 947 this.conflictIds = conflictIds; 948 this.items = Collections.unmodifiableCollection(items); 949 } 950 951 /** 952 * Gets the root node of the dependency graph being transformed. 953 * 954 * @return The root node of the dependency graph, never {@code null}. 955 */ 956 @Override 957 public DependencyNode getRoot() { 958 return root; 959 } 960 961 /** 962 * Determines whether the specified dependency node belongs to this conflict context. 963 * 964 * @param node The dependency node to check, must not be {@code null}. 965 * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise. 966 */ 967 @Override 968 public boolean isIncluded(DependencyNode node) { 969 return conflictId.equals(conflictIds.get(node)); 970 } 971 972 /** 973 * Gets the collection of conflict items in this context. 974 * 975 * @return The (read-only) collection of conflict items in this context, never {@code null}. 976 */ 977 @Override 978 public Collection<ConflictResolver.ConflictItem> getItems() { 979 return items; 980 } 981 982 /** 983 * Gets the conflict item which has been selected as the winner among the conflicting dependencies. 984 * 985 * @return The winning conflict item or {@code null} if not set yet. 986 */ 987 @Override 988 public ConflictResolver.ConflictItem getWinner() { 989 return winner; 990 } 991 992 /** 993 * Sets the conflict item which has been selected as the winner among the conflicting dependencies. 994 * 995 * @param winner The winning conflict item, may be {@code null}. 996 */ 997 @Override 998 public void setWinner(ConflictResolver.ConflictItem winner) { 999 this.winner = winner; 1000 } 1001 1002 /** 1003 * Gets the effective scope of the winning dependency. 1004 * 1005 * @return The effective scope of the winning dependency or {@code null} if none. 1006 */ 1007 @Override 1008 public String getScope() { 1009 return scope; 1010 } 1011 1012 /** 1013 * Sets the effective scope of the winning dependency. 1014 * 1015 * @param scope The effective scope, may be {@code null}. 1016 */ 1017 @Override 1018 public void setScope(String scope) { 1019 this.scope = scope; 1020 } 1021 1022 /** 1023 * Gets the effective optional flag of the winning dependency. 1024 * 1025 * @return The effective optional flag or {@code null} if none. 1026 */ 1027 @Override 1028 public Boolean getOptional() { 1029 return optional; 1030 } 1031 1032 /** 1033 * Sets the effective optional flag of the winning dependency. 1034 * 1035 * @param optional The effective optional flag, may be {@code null}. 1036 */ 1037 @Override 1038 public void setOptional(Boolean optional) { 1039 this.optional = optional; 1040 } 1041 1042 @Override 1043 public String toString() { 1044 return winner + " @ " + scope + " < " + items; 1045 } 1046 } 1047}