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