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