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.IdentityHashMap;
027import java.util.Iterator;
028import java.util.List;
029import java.util.ListIterator;
030import java.util.Map;
031import java.util.Objects;
032
033import org.eclipse.aether.RepositoryException;
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.artifact.ArtifactIdUtils;
040
041import static java.util.Objects.requireNonNull;
042
043/**
044 * A legacy dependency graph transformer that resolves version and scope conflicts among dependencies.
045 * This implementation maintains backward compatibility with Maven 3.x and Resolver 1.x behavior but has
046 * O(N²) worst-case performance characteristics. For new projects, consider using {@link PathConflictResolver}.
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²) worst-case where N is the number of dependency nodes</li>
057 * <li><strong>Memory Usage:</strong> Modifies the dependency graph in-place</li>
058 * <li><strong>Scalability:</strong> Performance degrades significantly on large multi-module projects</li>
059 * </ul>
060 * <p>
061 * <strong>Algorithm Overview:</strong>
062 * <ol>
063 * <li><strong>Depth-First Traversal:</strong> Walks the dependency graph depth-first</li>
064 * <li><strong>In-Place Modification:</strong> Modifies nodes directly during traversal</li>
065 * <li><strong>Conflict Detection:</strong> Identifies conflicts by comparing conflict IDs</li>
066 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners</li>
067 * <li><strong>Loser Removal:</strong> Removes or marks losing nodes during traversal</li>
068 * </ol>
069 * <p>
070 * <strong>When to Use:</strong>
071 * <ul>
072 * <li>Exact backward compatibility with Maven 3.x/Resolver 1.x is required</li>
073 * <li>Debugging performance differences between old and new algorithms</li>
074 * <li>Small projects where performance is not a concern</li>
075 * <li>Testing and validation scenarios</li>
076 * </ul>
077 * <p>
078 * <strong>Migration Recommendation:</strong> New projects should use {@link PathConflictResolver} for better
079 * performance. This implementation is retained primarily for compatibility and testing purposes.
080 * <p>
081 * <strong>Implementation Note:</strong> This conflict resolver is identical to the one used in Maven 3/Resolver 1.x.
082 * The implementation may produce O(N²) worst-case performance on projects with many small conflict groups
083 * (typically one member each), which is common in large multi-module projects.
084 *
085 * @see PathConflictResolver
086 * @since 2.0.11
087 */
088public final class ClassicConflictResolver extends ConflictResolver {
089    private final ConflictResolver.VersionSelector versionSelector;
090    private final ConflictResolver.ScopeSelector scopeSelector;
091    private final ConflictResolver.ScopeDeriver scopeDeriver;
092    private final ConflictResolver.OptionalitySelector optionalitySelector;
093
094    /**
095     * Creates a new conflict resolver instance with the specified hooks.
096     *
097     * @param versionSelector The version selector to use, must not be {@code null}.
098     * @param scopeSelector The scope selector to use, must not be {@code null}.
099     * @param optionalitySelector The optionality selector ot use, must not be {@code null}.
100     * @param scopeDeriver The scope deriver to use, must not be {@code null}.
101     */
102    public ClassicConflictResolver(
103            ConflictResolver.VersionSelector versionSelector,
104            ConflictResolver.ScopeSelector scopeSelector,
105            ConflictResolver.OptionalitySelector optionalitySelector,
106            ConflictResolver.ScopeDeriver scopeDeriver) {
107        this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null");
108        this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null");
109        this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null");
110        this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null");
111    }
112
113    @SuppressWarnings("unchecked")
114    @Override
115    public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
116            throws RepositoryException {
117        requireNonNull(node, "node cannot be null");
118        requireNonNull(context, "context cannot be null");
119        List<String> sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
120        if (sortedConflictIds == null) {
121            ConflictIdSorter sorter = new ConflictIdSorter();
122            sorter.transformGraph(node, context);
123
124            sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
125        }
126
127        @SuppressWarnings("unchecked")
128        Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
129        long time1 = System.nanoTime();
130
131        @SuppressWarnings("unchecked")
132        Collection<Collection<String>> conflictIdCycles =
133                (Collection<Collection<String>>) context.get(TransformationContextKeys.CYCLIC_CONFLICT_IDS);
134        if (conflictIdCycles == null) {
135            throw new RepositoryException("conflict id cycles have not been identified");
136        }
137
138        Map<DependencyNode, String> conflictIds =
139                (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
140        if (conflictIds == null) {
141            throw new RepositoryException("conflict groups have not been identified");
142        }
143
144        Map<String, Collection<String>> cyclicPredecessors = new HashMap<>();
145        for (Collection<String> cycle : conflictIdCycles) {
146            for (String conflictId : cycle) {
147                Collection<String> predecessors = cyclicPredecessors.computeIfAbsent(conflictId, k -> new HashSet<>());
148                predecessors.addAll(cycle);
149            }
150        }
151
152        State state = new State(node, conflictIds, sortedConflictIds.size(), context);
153        for (Iterator<String> it = sortedConflictIds.iterator(); it.hasNext(); ) {
154            String conflictId = it.next();
155
156            // reset data structures for next graph walk
157            state.prepare(conflictId, cyclicPredecessors.get(conflictId));
158
159            // find nodes with the current conflict id and while walking the graph (more deeply), nuke leftover losers
160            gatherConflictItems(node, state);
161
162            // now that we know the min depth of the parents, update depth of conflict items
163            state.finish();
164
165            // earlier runs might have nuked all parents of the current conflict id, so it might not exist anymore
166            if (!state.items.isEmpty()) {
167                ConflictContext ctx = state.conflictCtx;
168                state.versionSelector.selectVersion(ctx);
169                if (ctx.winner == null) {
170                    throw new RepositoryException("conflict resolver did not select winner among " + state.items);
171                }
172                DependencyNode winner = ((ConflictItem) (ctx.winner)).node;
173
174                state.scopeSelector.selectScope(ctx);
175                if (ConflictResolver.Verbosity.NONE != state.verbosity) {
176                    winner.setData(
177                            ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
178                            winner.getDependency().getScope());
179                }
180                winner.setScope(ctx.scope);
181
182                state.optionalitySelector.selectOptionality(ctx);
183                if (ConflictResolver.Verbosity.NONE != state.verbosity) {
184                    winner.setData(
185                            ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
186                            winner.getDependency().isOptional());
187                }
188                winner.setOptional(ctx.optional);
189
190                removeLosers(state);
191            }
192
193            // record the winner so we can detect leftover losers during future graph walks
194            state.winner();
195
196            // in case of cycles, trigger final graph walk to ensure all leftover losers are gone
197            if (!it.hasNext() && !conflictIdCycles.isEmpty() && state.conflictCtx.winner != null) {
198                DependencyNode winner = ((ConflictItem) (state.conflictCtx).winner).node;
199                // Note: using non-existing key here (empty) as that one for sure was not met
200                state.prepare("", null);
201                gatherConflictItems(winner, state);
202            }
203        }
204
205        if (stats != null) {
206            long time2 = System.nanoTime();
207            stats.put("ConflictResolver.totalTime", time2 - time1);
208            stats.put("ConflictResolver.conflictItemCount", state.totalConflictItems);
209        }
210
211        return node;
212    }
213
214    private boolean gatherConflictItems(DependencyNode node, State state) throws RepositoryException {
215        String conflictId = state.conflictIds.get(node);
216        if (state.currentId.equals(conflictId)) {
217            // found it, add conflict item (if not already done earlier by another path)
218            state.add(node);
219            // we don't recurse here so we might miss losers beneath us, those will be nuked during future walks below
220        } else if (state.loser(node, conflictId)) {
221            // found a leftover loser (likely in a cycle) of an already processed conflict id, tell caller to nuke it
222            return false;
223        } else if (state.push(node, conflictId)) {
224            // found potential parent, no cycle and not visited before with the same derived scope, so recurse
225            for (Iterator<DependencyNode> it = node.getChildren().iterator(); it.hasNext(); ) {
226                DependencyNode child = it.next();
227                if (!gatherConflictItems(child, state)) {
228                    it.remove();
229                }
230            }
231            state.pop();
232        }
233        return true;
234    }
235
236    private static void removeLosers(State state) {
237        ConflictItem winner = ((ConflictItem) state.conflictCtx.winner);
238        String winnerArtifactId = ArtifactIdUtils.toId(winner.node.getArtifact());
239        List<DependencyNode> previousParent = null;
240        ListIterator<DependencyNode> childIt = null;
241        HashSet<String> toRemoveIds = new HashSet<>();
242        for (ConflictItem item : state.items) {
243            if (item == winner) {
244                continue;
245            }
246            if (item.parent != previousParent) {
247                childIt = item.parent.listIterator();
248                previousParent = item.parent;
249            }
250            while (childIt.hasNext()) {
251                DependencyNode child = childIt.next();
252                if (child == item.node) {
253                    // NONE: just remove it and done
254                    if (ConflictResolver.Verbosity.NONE == state.verbosity) {
255                        childIt.remove();
256                        break;
257                    }
258
259                    // STANDARD: doing extra bookkeeping to select "which nodes to remove"
260                    if (ConflictResolver.Verbosity.STANDARD == state.verbosity) {
261                        String childArtifactId = ArtifactIdUtils.toId(child.getArtifact());
262                        // if two IDs are equal, it means "there is nearest", not conflict per se.
263                        // In that case we do NOT allow this child to be removed (but remove others)
264                        // and this keeps us safe from iteration (and in general, version) ordering
265                        // as we explicitly leave out ID that is "nearest found" state.
266                        //
267                        // This tackles version ranges mostly, where ranges are turned into list of
268                        // several nodes in collector (as many were discovered, ie. from metadata), and
269                        // old code would just "mark" the first hit as conflict, and remove the rest,
270                        // even if rest could contain "more suitable" version, that is not conflicting/diverging.
271                        // This resulted in verbose mode transformed tree, that was misrepresenting things
272                        // for dependency convergence calculations: it represented state like parent node
273                        // depends on "wrong" version (diverge), while "right" version was present (but removed)
274                        // as well, as it was contained in parents version range.
275                        if (!Objects.equals(winnerArtifactId, childArtifactId)) {
276                            toRemoveIds.add(childArtifactId);
277                        }
278                    }
279
280                    // FULL: just record the facts
281                    DependencyNode loser = new DefaultDependencyNode(child);
282                    loser.setData(ConflictResolver.NODE_DATA_WINNER, winner.node);
283                    loser.setData(
284                            ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
285                            loser.getDependency().getScope());
286                    loser.setData(
287                            ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
288                            loser.getDependency().isOptional());
289                    loser.setScope(item.getScopes().iterator().next());
290                    loser.setChildren(Collections.emptyList());
291                    childIt.set(loser);
292                    item.node = loser;
293                    break;
294                }
295            }
296        }
297
298        // 2nd pass to apply "standard" verbosity: leaving only 1 loser, but with care
299        if (ConflictResolver.Verbosity.STANDARD == state.verbosity && !toRemoveIds.isEmpty()) {
300            previousParent = null;
301            for (ConflictItem item : state.items) {
302                if (item == winner) {
303                    continue;
304                }
305                if (item.parent != previousParent) {
306                    childIt = item.parent.listIterator();
307                    previousParent = item.parent;
308                }
309                while (childIt.hasNext()) {
310                    DependencyNode child = childIt.next();
311                    if (child == item.node) {
312                        String childArtifactId = ArtifactIdUtils.toId(child.getArtifact());
313                        if (toRemoveIds.contains(childArtifactId)
314                                && relatedSiblingsCount(child.getArtifact(), item.parent) > 1) {
315                            childIt.remove();
316                        }
317                        break;
318                    }
319                }
320            }
321        }
322
323        // there might still be losers beneath the winner (e.g. in case of cycles)
324        // those will be nuked during future graph walks when we include the winner in the recursion
325    }
326
327    private static long relatedSiblingsCount(Artifact artifact, List<DependencyNode> parent) {
328        String ga = artifact.getGroupId() + ":" + artifact.getArtifactId();
329        return parent.stream()
330                .map(DependencyNode::getArtifact)
331                .filter(a -> ga.equals(a.getGroupId() + ":" + a.getArtifactId()))
332                .count();
333    }
334
335    static final class NodeInfo {
336
337        /**
338         * The smallest depth at which the node was seen, used for "the" depth of its conflict items.
339         */
340        int minDepth;
341
342        /**
343         * The set of derived scopes the node was visited with, used to check whether an already seen node needs to be
344         * revisited again in context of another scope. To conserve memory, we start with {@code String} and update to
345         * {@code Set<String>} if needed.
346         */
347        Object derivedScopes;
348
349        /**
350         * The set of derived optionalities the node was visited with, used to check whether an already seen node needs
351         * to be revisited again in context of another optionality. To conserve memory, encoded as bit field (bit 0 ->
352         * optional=false, bit 1 -> optional=true).
353         */
354        int derivedOptionalities;
355
356        /**
357         * The conflict items which are immediate children of the node, used to easily update those conflict items after
358         * a new parent scope/optionality was encountered.
359         */
360        List<ConflictItem> children;
361
362        static final int CHANGE_SCOPE = 0x01;
363
364        static final int CHANGE_OPTIONAL = 0x02;
365
366        private static final int OPT_FALSE = 0x01;
367
368        private static final int OPT_TRUE = 0x02;
369
370        NodeInfo(int depth, String derivedScope, boolean optional) {
371            minDepth = depth;
372            derivedScopes = derivedScope;
373            derivedOptionalities = optional ? OPT_TRUE : OPT_FALSE;
374        }
375
376        @SuppressWarnings("unchecked")
377        int update(int depth, String derivedScope, boolean optional) {
378            if (depth < minDepth) {
379                minDepth = depth;
380            }
381            int changes;
382            if (derivedScopes.equals(derivedScope)) {
383                changes = 0;
384            } else if (derivedScopes instanceof Collection) {
385                changes = ((Collection<String>) derivedScopes).add(derivedScope) ? CHANGE_SCOPE : 0;
386            } else {
387                Collection<String> scopes = new HashSet<>();
388                scopes.add((String) derivedScopes);
389                scopes.add(derivedScope);
390                derivedScopes = scopes;
391                changes = CHANGE_SCOPE;
392            }
393            int bit = optional ? OPT_TRUE : OPT_FALSE;
394            if ((derivedOptionalities & bit) == 0) {
395                derivedOptionalities |= bit;
396                changes |= CHANGE_OPTIONAL;
397            }
398            return changes;
399        }
400
401        void add(ConflictItem item) {
402            if (children == null) {
403                children = new ArrayList<>(1);
404            }
405            children.add(item);
406        }
407    }
408
409    final class State {
410
411        /**
412         * The conflict id currently processed.
413         */
414        String currentId;
415
416        /**
417         * Stats counter.
418         */
419        int totalConflictItems;
420
421        /**
422         * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts.
423         */
424        final ConflictResolver.Verbosity verbosity;
425
426        /**
427         * A mapping from conflict id to winner node, helps to recognize nodes that have their effective
428         * scope&optionality set or are leftovers from previous removals.
429         */
430        final Map<String, DependencyNode> resolvedIds;
431
432        /**
433         * The set of conflict ids which could apply to ancestors of nodes with the current conflict id, used to avoid
434         * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account
435         * for cyclic dependencies.
436         */
437        final Collection<String> potentialAncestorIds;
438
439        /**
440         * The output from the conflict marker
441         */
442        final Map<DependencyNode, String> conflictIds;
443
444        /**
445         * The conflict items we have gathered so far for the current conflict id.
446         */
447        final List<ConflictItem> items;
448
449        /**
450         * The (conceptual) mapping from nodes to extra infos, technically keyed by the node's child list which better
451         * captures the identity of a node since we're basically concerned with effects towards children.
452         */
453        final Map<List<DependencyNode>, NodeInfo> infos;
454
455        /**
456         * The set of nodes on the DFS stack to detect cycles, technically keyed by the node's child list to match the
457         * dirty graph structure produced by the dependency collector for cycles.
458         */
459        final Map<List<DependencyNode>, Boolean> stack;
460
461        /**
462         * The stack of parent nodes.
463         */
464        final List<DependencyNode> parentNodes;
465
466        /**
467         * The stack of derived scopes for parent nodes.
468         */
469        final List<String> parentScopes;
470
471        /**
472         * The stack of derived optional flags for parent nodes.
473         */
474        final List<Boolean> parentOptionals;
475
476        /**
477         * The stack of node infos for parent nodes, may contain {@code null} which is used to disable creating new
478         * conflict items when visiting their parent again (conflict items are meant to be unique by parent-node combo).
479         */
480        final List<NodeInfo> parentInfos;
481
482        /**
483         * The conflict context passed to the version/scope/optionality selectors, updated as we move along rather than
484         * recreated to avoid tmp objects.
485         */
486        final ConflictContext conflictCtx;
487
488        /**
489         * The scope context passed to the scope deriver, updated as we move along rather than recreated to avoid tmp
490         * objects.
491         */
492        final ScopeContext scopeCtx;
493
494        /**
495         * The effective version selector, i.e. after initialization.
496         */
497        final ConflictResolver.VersionSelector versionSelector;
498
499        /**
500         * The effective scope selector, i.e. after initialization.
501         */
502        final ConflictResolver.ScopeSelector scopeSelector;
503
504        /**
505         * The effective scope deriver, i.e. after initialization.
506         */
507        final ConflictResolver.ScopeDeriver scopeDeriver;
508
509        /**
510         * The effective optionality selector, i.e. after initialization.
511         */
512        final ConflictResolver.OptionalitySelector optionalitySelector;
513
514        State(
515                DependencyNode root,
516                Map<DependencyNode, String> conflictIds,
517                int conflictIdCount,
518                DependencyGraphTransformationContext context)
519                throws RepositoryException {
520            this.conflictIds = conflictIds;
521            this.verbosity = ConflictResolver.getVerbosity(context.getSession());
522            potentialAncestorIds = new HashSet<>(conflictIdCount * 2);
523            resolvedIds = new HashMap<>(conflictIdCount * 2);
524            items = new ArrayList<>(256);
525            infos = new IdentityHashMap<>(64);
526            stack = new IdentityHashMap<>(64);
527            parentNodes = new ArrayList<>(64);
528            parentScopes = new ArrayList<>(64);
529            parentOptionals = new ArrayList<>(64);
530            parentInfos = new ArrayList<>(64);
531            conflictCtx = new ConflictContext(root, conflictIds, items);
532            scopeCtx = new ScopeContext(null, null);
533            versionSelector = ClassicConflictResolver.this.versionSelector.getInstance(root, context);
534            scopeSelector = ClassicConflictResolver.this.scopeSelector.getInstance(root, context);
535            scopeDeriver = ClassicConflictResolver.this.scopeDeriver.getInstance(root, context);
536            optionalitySelector = ClassicConflictResolver.this.optionalitySelector.getInstance(root, context);
537        }
538
539        void prepare(String conflictId, Collection<String> cyclicPredecessors) {
540            currentId = conflictId;
541            conflictCtx.conflictId = conflictId;
542            conflictCtx.winner = null;
543            conflictCtx.scope = null;
544            conflictCtx.optional = null;
545            items.clear();
546            infos.clear();
547            if (cyclicPredecessors != null) {
548                potentialAncestorIds.addAll(cyclicPredecessors);
549            }
550        }
551
552        void finish() {
553            List<DependencyNode> previousParent = null;
554            int previousDepth = 0;
555            totalConflictItems += items.size();
556            for (ListIterator<ConflictItem> iterator = items.listIterator(items.size()); iterator.hasPrevious(); ) {
557                ConflictItem item = iterator.previous();
558                if (item.parent == previousParent) {
559                    item.depth = previousDepth;
560                } else if (item.parent != null) {
561                    previousParent = item.parent;
562                    NodeInfo info = infos.get(previousParent);
563                    previousDepth = info.minDepth + 1;
564                    item.depth = previousDepth;
565                }
566            }
567            potentialAncestorIds.add(currentId);
568        }
569
570        void winner() {
571            resolvedIds.put(currentId, (conflictCtx.winner != null) ? ((ConflictItem) conflictCtx.winner).node : null);
572        }
573
574        boolean loser(DependencyNode node, String conflictId) {
575            DependencyNode winner = resolvedIds.get(conflictId);
576            return winner != null && winner != node;
577        }
578
579        boolean push(DependencyNode node, String conflictId) throws RepositoryException {
580            if (conflictId == null) {
581                if (node.getDependency() != null) {
582                    if (node.getData().get(ConflictResolver.NODE_DATA_WINNER) != null) {
583                        return false;
584                    }
585                    throw new RepositoryException("missing conflict id for node " + node);
586                }
587            } else if (!potentialAncestorIds.contains(conflictId)) {
588                return false;
589            }
590
591            List<DependencyNode> graphNode = node.getChildren();
592            if (stack.put(graphNode, Boolean.TRUE) != null) {
593                return false;
594            }
595
596            int depth = depth();
597            String scope = deriveScope(node, conflictId);
598            boolean optional = deriveOptional(node, conflictId);
599            NodeInfo info = infos.get(graphNode);
600            if (info == null) {
601                info = new NodeInfo(depth, scope, optional);
602                infos.put(graphNode, info);
603                parentInfos.add(info);
604                parentNodes.add(node);
605                parentScopes.add(scope);
606                parentOptionals.add(optional);
607            } else {
608                int changes = info.update(depth, scope, optional);
609                if (changes == 0) {
610                    stack.remove(graphNode);
611                    return false;
612                }
613                parentInfos.add(null); // disable creating new conflict items, we update the existing ones below
614                parentNodes.add(node);
615                parentScopes.add(scope);
616                parentOptionals.add(optional);
617                if (info.children != null) {
618                    if ((changes & NodeInfo.CHANGE_SCOPE) != 0) {
619                        ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size());
620                        while (itemIterator.hasPrevious()) {
621                            ConflictItem item = itemIterator.previous();
622                            String childScope = deriveScope(item.node, null);
623                            item.addScope(childScope);
624                        }
625                    }
626                    if ((changes & NodeInfo.CHANGE_OPTIONAL) != 0) {
627                        ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size());
628                        while (itemIterator.hasPrevious()) {
629                            ConflictItem item = itemIterator.previous();
630                            boolean childOptional = deriveOptional(item.node, null);
631                            item.addOptional(childOptional);
632                        }
633                    }
634                }
635            }
636
637            return true;
638        }
639
640        void pop() {
641            int last = parentInfos.size() - 1;
642            parentInfos.remove(last);
643            parentScopes.remove(last);
644            parentOptionals.remove(last);
645            DependencyNode node = parentNodes.remove(last);
646            stack.remove(node.getChildren());
647        }
648
649        void add(DependencyNode node) throws RepositoryException {
650            DependencyNode parent = parent();
651            if (parent == null) {
652                ConflictItem item = newConflictItem(parent, node);
653                items.add(item);
654            } else {
655                NodeInfo info = parentInfos.get(parentInfos.size() - 1);
656                if (info != null) {
657                    ConflictItem item = newConflictItem(parent, node);
658                    info.add(item);
659                    items.add(item);
660                }
661            }
662        }
663
664        private ConflictItem newConflictItem(DependencyNode parent, DependencyNode node) throws RepositoryException {
665            return new ConflictItem(parent, node, deriveScope(node, null), deriveOptional(node, null));
666        }
667
668        private int depth() {
669            return parentNodes.size();
670        }
671
672        private DependencyNode parent() {
673            int size = parentNodes.size();
674            return (size <= 0) ? null : parentNodes.get(size - 1);
675        }
676
677        private String deriveScope(DependencyNode node, String conflictId) throws RepositoryException {
678            if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0
679                    || (conflictId != null && resolvedIds.containsKey(conflictId))) {
680                return scope(node.getDependency());
681            }
682
683            int depth = parentNodes.size();
684            scopes(depth, node.getDependency());
685            if (depth > 0) {
686                scopeDeriver.deriveScope(scopeCtx);
687            }
688            return scopeCtx.derivedScope;
689        }
690
691        private void scopes(int parent, Dependency child) {
692            scopeCtx.parentScope = (parent > 0) ? parentScopes.get(parent - 1) : null;
693            scopeCtx.derivedScope = scope(child);
694            scopeCtx.childScope = scope(child);
695        }
696
697        private String scope(Dependency dependency) {
698            return (dependency != null) ? dependency.getScope() : null;
699        }
700
701        private boolean deriveOptional(DependencyNode node, String conflictId) {
702            Dependency dep = node.getDependency();
703            boolean optional = (dep != null) && dep.isOptional();
704            if (optional
705                    || (node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0
706                    || (conflictId != null && resolvedIds.containsKey(conflictId))) {
707                return optional;
708            }
709            int depth = parentNodes.size();
710            return (depth > 0) ? parentOptionals.get(depth - 1) : false;
711        }
712    }
713
714    /**
715     * A context used to hold information that is relevant for deriving the scope of a child dependency.
716     *
717     * @see ConflictResolver.ScopeDeriver
718     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
719     *                change without notice and only exists to enable unit testing.
720     */
721    private static final class ScopeContext extends ConflictResolver.ScopeContext {
722        private String parentScope;
723        private String childScope;
724        private String derivedScope;
725
726        /**
727         * Creates a new scope context with the specified properties.
728         *
729         * @param parentScope The scope of the parent dependency, may be {@code null}.
730         * @param childScope The scope of the child dependency, may be {@code null}.
731         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
732         *              change without notice and only exists to enable unit testing.
733         */
734        private ScopeContext(String parentScope, String childScope) {
735            this.parentScope = (parentScope != null) ? parentScope : "";
736            derivedScope = (childScope != null) ? childScope : "";
737            this.childScope = (childScope != null) ? childScope : "";
738        }
739
740        /**
741         * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
742         * the scope deriver.
743         *
744         * @return The scope of the parent dependency, never {@code null}.
745         */
746        @Override
747        public String getParentScope() {
748            return parentScope;
749        }
750
751        /**
752         * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
753         * descriptor of the parent dependency.
754         *
755         * @return The original scope of the child dependency, never {@code null}.
756         */
757        @Override
758        public String getChildScope() {
759            return childScope;
760        }
761
762        /**
763         * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
764         * scope deriver makes changes.
765         *
766         * @return The derived scope of the child dependency, never {@code null}.
767         */
768        @Override
769        public String getDerivedScope() {
770            return derivedScope;
771        }
772
773        /**
774         * Sets the derived scope of the child dependency.
775         *
776         * @param derivedScope The derived scope of the dependency, may be {@code null}.
777         */
778        @Override
779        public void setDerivedScope(String derivedScope) {
780            this.derivedScope = (derivedScope != null) ? derivedScope : "";
781        }
782    }
783
784    /**
785     * A conflicting dependency.
786     *
787     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
788     *                change without notice and only exists to enable unit testing.
789     */
790    private static final class ConflictItem extends ConflictResolver.ConflictItem {
791
792        // nodes can share child lists, we care about the unique owner of a child node which is the child list
793        final List<DependencyNode> parent;
794
795        // only for debugging/toString() to help identify the parent node(s)
796        final Artifact artifact;
797
798        // is mutable as removeLosers will mutate it (if Verbosity==STANDARD)
799        DependencyNode node;
800
801        int depth;
802
803        // we start with String and update to Set<String> if needed
804        Object scopes;
805
806        // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE
807        int optionalities;
808
809        /**
810         * Bit flag indicating whether one or more paths consider the dependency non-optional.
811         */
812        public static final int OPTIONAL_FALSE = 0x01;
813
814        /**
815         * Bit flag indicating whether one or more paths consider the dependency optional.
816         */
817        public static final int OPTIONAL_TRUE = 0x02;
818
819        private ConflictItem(DependencyNode parent, DependencyNode node, String scope, boolean optional) {
820            if (parent != null) {
821                this.parent = parent.getChildren();
822                this.artifact = parent.getArtifact();
823            } else {
824                this.parent = null;
825                this.artifact = null;
826            }
827            this.node = node;
828            this.scopes = scope;
829            this.optionalities = optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
830        }
831
832        /**
833         * Determines whether the specified conflict item is a sibling of this item.
834         *
835         * @param item The other conflict item, must not be {@code null}.
836         * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise.
837         */
838        @Override
839        public boolean isSibling(ConflictResolver.ConflictItem item) {
840            return parent == ((ConflictItem) item).parent;
841        }
842
843        /**
844         * Gets the dependency node involved in the conflict.
845         *
846         * @return The involved dependency node, never {@code null}.
847         */
848        @Override
849        public DependencyNode getNode() {
850            return node;
851        }
852
853        /**
854         * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
855         *
856         * @return The involved dependency, never {@code null}.
857         */
858        @Override
859        public Dependency getDependency() {
860            return node.getDependency();
861        }
862
863        /**
864         * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
865         * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
866         * possible depth.
867         *
868         * @return The zero-based depth of the node in the graph.
869         */
870        @Override
871        public int getDepth() {
872            return depth;
873        }
874
875        /**
876         * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
877         * different paths and each path might result in a different derived scope.
878         *
879         * @see ConflictResolver.ScopeDeriver
880         * @return The (read-only) set of derived scopes of the dependency, never {@code null}.
881         */
882        @SuppressWarnings("unchecked")
883        @Override
884        public Collection<String> getScopes() {
885            if (scopes instanceof String) {
886                return Collections.singleton((String) scopes);
887            }
888            return (Collection<String>) scopes;
889        }
890
891        @SuppressWarnings("unchecked")
892        void addScope(String scope) {
893            if (scopes instanceof Collection) {
894                ((Collection<String>) scopes).add(scope);
895            } else if (!scopes.equals(scope)) {
896                Collection<String> set = new HashSet<>();
897                set.add((String) scopes);
898                set.add(scope);
899                scopes = set;
900            }
901        }
902
903        /**
904         * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
905         * different paths and each path might result in a different derived optionality.
906         *
907         * @return A bit field consisting of {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
908         *         {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
909         *         dependency was encountered with.
910         */
911        @Override
912        public int getOptionalities() {
913            return optionalities;
914        }
915
916        void addOptional(boolean optional) {
917            optionalities |= optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
918        }
919
920        @Override
921        public String toString() {
922            return node + " @ " + depth + " < " + artifact;
923        }
924    }
925
926    /**
927     * A context used to hold information that is relevant for resolving version and scope conflicts.
928     *
929     * @see ConflictResolver.VersionSelector
930     * @see ConflictResolver.ScopeSelector
931     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
932     *                change without notice and only exists to enable unit testing.
933     */
934    private static final class ConflictContext extends ConflictResolver.ConflictContext {
935        final DependencyNode root;
936        final Map<DependencyNode, String> conflictIds;
937        final Collection<ConflictResolver.ConflictItem> items;
938
939        String conflictId;
940        ConflictResolver.ConflictItem winner;
941        String scope;
942        Boolean optional;
943
944        private ConflictContext(
945                DependencyNode root, Map<DependencyNode, String> conflictIds, Collection<ConflictItem> items) {
946            this.root = root;
947            this.conflictIds = conflictIds;
948            this.items = Collections.unmodifiableCollection(items);
949        }
950
951        /**
952         * Gets the root node of the dependency graph being transformed.
953         *
954         * @return The root node of the dependency graph, never {@code null}.
955         */
956        @Override
957        public DependencyNode getRoot() {
958            return root;
959        }
960
961        /**
962         * Determines whether the specified dependency node belongs to this conflict context.
963         *
964         * @param node The dependency node to check, must not be {@code null}.
965         * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise.
966         */
967        @Override
968        public boolean isIncluded(DependencyNode node) {
969            return conflictId.equals(conflictIds.get(node));
970        }
971
972        /**
973         * Gets the collection of conflict items in this context.
974         *
975         * @return The (read-only) collection of conflict items in this context, never {@code null}.
976         */
977        @Override
978        public Collection<ConflictResolver.ConflictItem> getItems() {
979            return items;
980        }
981
982        /**
983         * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
984         *
985         * @return The winning conflict item or {@code null} if not set yet.
986         */
987        @Override
988        public ConflictResolver.ConflictItem getWinner() {
989            return winner;
990        }
991
992        /**
993         * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
994         *
995         * @param winner The winning conflict item, may be {@code null}.
996         */
997        @Override
998        public void setWinner(ConflictResolver.ConflictItem winner) {
999            this.winner = winner;
1000        }
1001
1002        /**
1003         * Gets the effective scope of the winning dependency.
1004         *
1005         * @return The effective scope of the winning dependency or {@code null} if none.
1006         */
1007        @Override
1008        public String getScope() {
1009            return scope;
1010        }
1011
1012        /**
1013         * Sets the effective scope of the winning dependency.
1014         *
1015         * @param scope The effective scope, may be {@code null}.
1016         */
1017        @Override
1018        public void setScope(String scope) {
1019            this.scope = scope;
1020        }
1021
1022        /**
1023         * Gets the effective optional flag of the winning dependency.
1024         *
1025         * @return The effective optional flag or {@code null} if none.
1026         */
1027        @Override
1028        public Boolean getOptional() {
1029            return optional;
1030        }
1031
1032        /**
1033         * Sets the effective optional flag of the winning dependency.
1034         *
1035         * @param optional The effective optional flag, may be {@code null}.
1036         */
1037        @Override
1038        public void setOptional(Boolean optional) {
1039            this.optional = optional;
1040        }
1041
1042        @Override
1043        public String toString() {
1044            return winner + " @ " + scope + " < " + items;
1045        }
1046    }
1047}