View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.util.graph.transformer;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.IdentityHashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.ListIterator;
30  import java.util.Map;
31  import java.util.Objects;
32  
33  import org.eclipse.aether.RepositoryException;
34  import org.eclipse.aether.artifact.Artifact;
35  import org.eclipse.aether.collection.DependencyGraphTransformationContext;
36  import org.eclipse.aether.graph.DefaultDependencyNode;
37  import org.eclipse.aether.graph.Dependency;
38  import org.eclipse.aether.graph.DependencyNode;
39  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
40  
41  import static java.util.Objects.requireNonNull;
42  
43  /**
44   * A legacy dependency graph transformer that resolves version and scope conflicts among dependencies.
45   * This implementation maintains backward compatibility with Maven 3.x and Resolver 1.x behavior but has
46   * O(N²) worst-case performance characteristics. For new projects, consider using {@link PathConflictResolver}.
47   * <p>
48   * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
49   * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
50   * be left in place with conflict information. The exact rules by which a winning node and its effective scope
51   * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
52   * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
53   * <p>
54   * <strong>Performance Characteristics:</strong>
55   * <ul>
56   * <li><strong>Time Complexity:</strong> O(N²) worst-case where N is the number of dependency nodes</li>
57   * <li><strong>Memory Usage:</strong> Modifies the dependency graph in-place</li>
58   * <li><strong>Scalability:</strong> Performance degrades significantly on large multi-module projects</li>
59   * </ul>
60   * <p>
61   * <strong>Algorithm Overview:</strong>
62   * <ol>
63   * <li><strong>Depth-First Traversal:</strong> Walks the dependency graph depth-first</li>
64   * <li><strong>In-Place Modification:</strong> Modifies nodes directly during traversal</li>
65   * <li><strong>Conflict Detection:</strong> Identifies conflicts by comparing conflict IDs</li>
66   * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners</li>
67   * <li><strong>Loser Removal:</strong> Removes or marks losing nodes during traversal</li>
68   * </ol>
69   * <p>
70   * <strong>When to Use:</strong>
71   * <ul>
72   * <li>Exact backward compatibility with Maven 3.x/Resolver 1.x is required</li>
73   * <li>Debugging performance differences between old and new algorithms</li>
74   * <li>Small projects where performance is not a concern</li>
75   * <li>Testing and validation scenarios</li>
76   * </ul>
77   * <p>
78   * <strong>Migration Recommendation:</strong> New projects should use {@link PathConflictResolver} for better
79   * performance. This implementation is retained primarily for compatibility and testing purposes.
80   * <p>
81   * <strong>Implementation Note:</strong> This conflict resolver is identical to the one used in Maven 3/Resolver 1.x.
82   * The implementation may produce O(N²) worst-case performance on projects with many small conflict groups
83   * (typically one member each), which is common in large multi-module projects.
84   *
85   * @see PathConflictResolver
86   * @since 2.0.11
87   */
88  public final class ClassicConflictResolver extends ConflictResolver {
89      private final ConflictResolver.VersionSelector versionSelector;
90      private final ConflictResolver.ScopeSelector scopeSelector;
91      private final ConflictResolver.ScopeDeriver scopeDeriver;
92      private final ConflictResolver.OptionalitySelector optionalitySelector;
93  
94      /**
95       * Creates a new conflict resolver instance with the specified hooks.
96       *
97       * @param versionSelector The version selector to use, must not be {@code null}.
98       * @param scopeSelector The scope selector to use, must not be {@code null}.
99       * @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 }