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