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