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