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