001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util.graph.transformer;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028
029import org.eclipse.aether.ConfigurationProperties;
030import org.eclipse.aether.RepositoryException;
031import org.eclipse.aether.RepositorySystemSession;
032import org.eclipse.aether.artifact.Artifact;
033import org.eclipse.aether.collection.DependencyGraphTransformationContext;
034import org.eclipse.aether.graph.DefaultDependencyNode;
035import org.eclipse.aether.graph.Dependency;
036import org.eclipse.aether.graph.DependencyNode;
037import org.eclipse.aether.util.ConfigUtils;
038import org.eclipse.aether.util.artifact.ArtifactIdUtils;
039
040import static java.util.Objects.requireNonNull;
041
042/**
043 * A high-performance dependency graph transformer that resolves version and scope conflicts among dependencies.
044 * This is the recommended conflict resolver implementation that provides O(N) performance characteristics,
045 * significantly improving upon the O(N²) worst-case performance of {@link ClassicConflictResolver}.
046 * <p>
047 * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
048 * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
049 * be left in place with conflict information. The exact rules by which a winning node and its effective scope
050 * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
051 * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
052 * <p>
053 * <strong>Performance Characteristics:</strong>
054 * <ul>
055 * <li><strong>Time Complexity:</strong> O(N) where N is the number of dependency nodes</li>
056 * <li><strong>Memory Usage:</strong> Creates a parallel tree structure for conflict-free processing</li>
057 * <li><strong>Scalability:</strong> Excellent performance on large multi-module projects</li>
058 * </ul>
059 * <p>
060 * <strong>Algorithm Overview:</strong>
061 * <ol>
062 * <li><strong>Path Tree Construction:</strong> Builds a cycle-free parallel tree structure from the input
063 *     dependency graph, where each {@code Path} represents a unique route to a dependency node</li>
064 * <li><strong>Conflict Partitioning:</strong> Groups paths by conflict ID (based on groupId:artifactId:classifier:extension coordinates)</li>
065 * <li><strong>Topological Processing:</strong> Processes conflict groups in topologically sorted order</li>
066 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners within each conflict group</li>
067 * <li><strong>Graph Transformation:</strong> Applies changes back to the original dependency graph</li>
068 * </ol>
069 * <p>
070 * <strong>Key Differences from {@link ClassicConflictResolver}:</strong>
071 * <ul>
072 * <li><strong>Performance:</strong> O(N) vs O(N²) time complexity</li>
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>When to Use:</strong>
079 * <ul>
080 * <li>Default choice for all new projects and Maven 4+ installations</li>
081 * <li>Large multi-module projects with many dependencies</li>
082 * <li>Performance-critical build environments</li>
083 * <li>Any scenario where {@link ClassicConflictResolver} shows performance bottlenecks</li>
084 * </ul>
085 * <p>
086 * <strong>Implementation Note:</strong> This conflict resolver builds a cycle-free "parallel" structure based on the
087 * passed-in dependency graph, and applies operations level by level starting from the root. The parallel {@code Path}
088 * tree ensures that cycles in the original graph don't affect the conflict resolution algorithm's performance.
089 *
090 * @see ClassicConflictResolver
091 * @since 2.0.11
092 */
093public final class PathConflictResolver extends ConflictResolver {
094    /**
095     * This implementation of conflict resolver is able to show more precise information regarding cycles in standard
096     * verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it
097     * may want to enable this for easier cycle detection, but in that case this conflict resolver will provide "extra nodes"
098     * not present on "standard verbosity level" with "classic" conflict resolver, that may lead to IT issues down the
099     * 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}